koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

Vimで<C-c>を含むマッピングをするときに注意すること

前書き

まずは,簡単な説明をする.

$ vim -u NONE

としてvimを立ち上げて, :nnoremap :<C-u>a<C-c>echo 'Hello World!'<CR> のようにマッピングしてみよう. そして,ノーマルモードにて, a<C-c> と入力すると, Hello World! とエコーされず,

Type  :quit<Enter>  to exit Vim

と表示されると思う. (このメッセージはロケールに依存する.上記は language messages C のときの例)

本題

<C-c> を含むマッピングを行うときには注意しなければならない. vim-jpのissueにあるように,

nnoremap z<C-c> :<C-u>echo 'key mapping works'<CR>

マッピングをして,ノーマルモードで z<C-c> と押すと <C-c> のデフォルトの動作をしてしまう. だが,

nnoremap <C-c> <NOP>

のように, <C-c> に対して何か適当なマッピングを行い,再度 z<C-c> と押すと,

key mapping works

と表示される. 古いVimであれば,

nunmap <C-c>

として, z<C-c> と入力しても,

key mapping works

と表示されていたが,いつからか(Vim 8.4.xxx 以降あたり)から再び

Type  :quit<Enter>  to exit Vim

と表示されるようになり,一貫性のある動作をするようになった.

このように <C-c> 単体に何らかのマッピングをしていない場合, <C-c> を含むマッピングができないので,

nnoremap <C-c> <NOP>

もしくは,

nnoremap <C-c> <C-c>

マッピングしておくことをオススメする. 後者であれば,デフォルトの <C-c> 単体の動作を消すことなく, <C-c> を含むマッピングが可能になる.

この記事中ではノーマルモードでのマッピングのみ取り上げたが,他のモードでのマッピングについても同様である.

参考

VimでUndo履歴を消去する

:help clear-undo を見ると,

let old_undolevels = &undolevels
set undolevels=-1
exe "normal a \<BS>\<Esc>"
let &undolevels = old_undolevels
unlet old_undolevels

とすれば,undo履歴が消去できるとのこと. しかし,コマンド normal は,マッピングを展開するので, normal! を用いた方がよい. また,オプション undolevels の値を一時的とはいえ,グローバルに変更する必要は無いので,以下のようにした方がより望ましいだろう.

let old_undolevels = &l:undolevels
setlocal undolevels=-1
execute "normal! a \<BS>\<Esc>"
let &l:undolevels = old_undolevels
unlet old_undolevels

これをコマンド化し,

function! s:clear_undo() abort
  let old_undolevels = &undolevels
  setlocal undolevels=-1
  execute "normal! a \<BS>\<Esc>"
  let &l:undolevels = old_undolevels
endfunction
command! -bar ClearUndo  call s:clear_undo()

とするのも良いだろう. (Undo履歴を消したいというのはかなりレアなケースだと思うが)

Vimでルートユーザとして保存する

rootユーザでVimを起動したり, sudoeditVimを起動しなかった場合,権限の無いファイルを編集後,保存しようとしても保存できない. 割とあるあるなことだと思う. (特にVimに閉じこもる人にとっては)

そんなとき,

:w !sudo tee > /dev/null %

と実行することで,ルートユーザとして保存できる. これはよく紹介されている手法で,書籍「実践Vim 思考のスピードで編集しよう! 」でも紹介されている. 簡単にググっただけでも,

のように数多くの記事で紹介されている. 上で紹介した記事や,人の.vimrcでよく見掛けるのが,

cnoremap w!! w !sudo tee > /dev/null %

cabbrev w!! w !sudo tee > /dev/null %

という設定だが,個人的にはコマンドにまとめておく方が好きである.

if executable('sudo')
  function! s:save_as_root(bang, filename) abort
    execute 'write' . a:bang '!sudo tee > /dev/null' (a:filename ==# '' ? '%' : a:filename)
  endfunction
else
  function! s:save_as_root(bang, filename) abort
    echoerr 'sudo is not supported in this environment'
  endfunction
endif
command! -bar -bang -nargs=? -complete=file SudoWrite  call s:save_as_root('<bang>', <q-args>)

これを, w[rite] コマンドと同じように, :SudoWrite と実行するとよい. なお,コマンドが呼び出すのはたった1行の関数なので,わざわざ関数を作る必要はないかもしれない.

if executable('sudo')
  command! -bar -bang -nargs=? -complete=file SudoWrite
        \ execute 'write' . '<bang>' '!sudo tee > /dev/null' (<q-args> ==# '' ? '%' : <q-args>)
else
  command! -bar -bang -nargs=? -complete=file SudoWrite
        \ echoerr 'sudo is not supported in this environment'
endif

正直, bang に関してはあまり意味は無いが,元々の :writebang を取ることができるので,ついでに指定しておいただけだ.

参考

NeoBundleLazyのautoloadのcommands指定で頑張った話

neobundle.vimには,遅延読み込み機能があり,適切に設定してやることで,プラグインの使用感はそのままで,Vimの起動時間を短縮することができる. autoloadの設定には,

  • ファイルタイプ検出時 filetypes
  • ファイル名検出時 filename_patterns
  • コマンド実行時 commands
  • 関数実行時 functions, function_prefix
  • マップ実行時 mappings
  • Uniteのソース読み込み時 unite_sources
  • インサートモードに入った時 insert

など,様々なタイミングで,プラグインのパスを runtimepath に追加し,読み込みを行うことができる. コマンドの実行時の検出は,仮のコマンドを定義しておいて,それが実行されたとき,プラグイン本体を読み込む仕組みとなっていると思われる. そして,仮のコマンドには引数の補完関数を指定することもできる. basyura/TweetVimのNeoBundleLazyの設定を例にとると,以下のような形になるだろう.

NeoBundleLazy 'basyura/TweetVim'
if neobundle#tap('TweetVim')
  call neobundle#config({
        \ 'depends': ['tyru/open-browser.vim', 'basyura/twibill.vim'],
        \ 'autoload': {
        \   'commands': [
        \     'TweetVimAccessToken',
        \     'TweetVimAddAccount',
        \     'TweetVimClearIcon',
        \     'TweetVimCommandSay',
        \     'TweetVimCurrentLineSay',
        \     'TweetVimHomeTimeline',
        \     {'name': 'TweetVimListStatuses', 'complete': 'custom,tweetvim#complete#list'},
        \     'TweetVimMentions',
        \     {'name': 'TweetVimSay', 'complete': 'custom,tweetvim#complete#account'},
        \     {'name': 'TweetVimSearch', 'complete': 'custom,tweetvim#complete#search'},
        \     {'name': 'TweetVimSwitchAccount', 'complete': 'custom,tweetvim#complete#account'},
        \     'TweetVimUserStream',
        \     {'name': 'TweetVimUserTimeline', 'complete': 'custom,tweetvim#complete#screen_name'},
        \     'TweetVimVersion'
        \   ],
        \   'unite_sources': 'tweetvim'
        \ }
        \})
  call neobundle#untap()
endif

この例のautoloadのcommands設定にあるように,コマンドの補完関数には, customcustomlist に,プラグインautoload/hoge.vim で定義されている関数を指定してもよい. その場合,コマンド引数のTab補完を行った瞬間に,プラグインのロードが行われる.

しかし,プラグインによっては, customcustomlist に, plugin/hoge.vimスクリプトローカル関数を指定している場合もある. スクリプトローカル関数はneobundle側からは見えないので,指定することはできない. プラグインのロードが行われるまで我慢してもよいが,少し気分が悪い.

そこで,.vimrcに plugin/hoge.vim で定義されている関数をコピペし,neobundleから見えるようにスクリプト番号と共に渡してやるとよい. その補完関数はプラグインのロード後には不必要になるので,プラグイン読み込み時に呼び出されるフック関数で消去する,あるいは消去するオートコマンドを定義する.

そういったプラグインの例として,mattn/gist-vimosyo-manga/vim-reanimateなどがあるので,その2つのプラグインを例にとって,設定例を紹介する.

if has('vim_starting')
  set rtp+=~/.vim/bundle/neobundle.vim
endif
call neobundle#begin()
NeoBundleFetch 'Shougo/neobundle.vim'

let g:neobundle#default_options = {'_': {'verbose': 1}}
augroup CompleteDummy
  autocmd!
augroup END

function! s:SID() abort
  return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$')
endfun
let s:sid = s:SID()
delfunction s:SID

function! s:to_global_name(scriptlocal_funcname) abort
  return '<SNR>' . s:sid . '_' . a:scriptlocal_funcname
endfunction

" 適当なイベントが発生したら,補完関数を消去
function! s:delete_function_lazy(funcname) abort
  execute 'autocmd CompleteDummy CursorHold,CursorHoldI,CursorMoved,CursorMovedI,InsertEnter *'
        \ 'delfunction' a:funcname
        \ '| autocmd! CompleteDummy CursorHold,CursorHoldI,CursorMoved,CursorMovedI,InsertEnter *'
endfunction

if neobundle#load_cache()
  NeoBundleLazy 'mattn/gist-vim'
  NeoBundleLazy 'osyo-manga/vim-reanimate'

  " キャッシュされる設定はこっちに書く
  if neobundle#tap('gist-vim')
    call neobundle#config({
          \ 'autoload': {
          \   'commands': {'name': 'Gist', 'complete': 'customlist,' . s:to_global_name('gist_CompleteArgs')}
          \ }
          \})
    call neobundle#untap()
  endif

  if neobundle#tap('vim-reanimate')
    let s:_ = 'customlist,' . s:to_global_name('reanimate_save_point_completelist')
    call neobundle#config({
          \ 'autoload': {
          \   'commands': [
          \     {'name': 'ReanimateSave', 'complete': s:_},
          \     'ReanimateSaveCursorHold',
          \     'ReanimateSaveInput',
          \     {'name': 'ReanimateLoad', 'complete': s:_},
          \     'ReanimateLoadInput',
          \     'ReanimateLoadLatest',
          \     {'name': 'ReanimateSwitch', 'complete': s:_},
          \     {'name': 'ReanimateEditVimrcLocal', 'complete': s:_},
          \     'ReanimateUnLoad'
          \   ]
          \ }
          \})
    unlet s:_
    call neobundle#untap()
  endif
  NeoBundleSaveCache
endif
call neobundle#end()

" キャッシュされないグローバル変数の設定等はこっち
if neobundle#tap('gist-vim')
  " plugin/gist.vim を見てコピペ
  function! s:gist_CompleteArgs(arg_lead,cmdline,cursor_pos) abort
    return filter(["-p", "-P", "-a", "-m", "-e", "-s", "-d", "+1", "-1", "-f", "-c", "-l", "-la", "-ls", "-b",
          \ "--listall", "--liststar", "--list", "--multibuffer", "--private", "--public", "--anonymous", "--description", "--clipboard",
          \ "--rawurl", "--delete", "--edit", "--star", "--unstar", "--fork", "--browser"
          \ ], '!stridx(v:val, a:arg_lead)')
  endfunction
  function! neobundle#tapped.hooks.on_post_source(bundle) abort
    delfunction s:gist_CompleteArgs
  endfunction
  call neobundle#untap()
endif

if neobundle#tap('vim-reanimate')
  " plugin/reanimate.vim を見てコピペ
  function! s:reanimate_save_point_completelist(arglead, ...) abort
    return filter(reanimate#save_points(), "v:val =~? '" . a:arglead . "'")
  endfunction
  " neobundle#tapped.hooks.on_source() でもいい
  function! neobundle#tapped.hooks.on_post_source(bundle) abort
    " 一時的な補完関数の中でautoload関数をコールをしているので,
    " このタイミングで補完関数は消去できない
    " なので,適当なタイミングで一時的な補完関数を消去する
    call s:delete_function_lazy('s:reanimate_save_point_completelist')
  endfunction
  let g:reanimate_save_dir = '~/.vim/save'
  let g:reanimate_default_save_name = 'reanimate'
  let g:reanimate_sessionoptions = 'curdir,folds,help,localoptions,slash,tabpages,winsize'
  call neobundle#untap()
endif

if !has('vim_starting')
  call neobundle#call_hook('on_source')
endif
filetype plugin indent on

Vimに自殺させる

Vim力を上げるために,過去のvimrc読書会のログを見ていたときに,次のコマンドを発見した.

command! Suicide  call system('kill -KILL ' . getpid())

Vimは自分自身のPIDを getpid() 関数で取得できる. そうして取得した自身のPIDに対して kill コマンドを実行して,自殺しようというわけだ.

ただし, kill コマンドはWindowsには無い. Windowskill コマンドに相当するのは, taskkill コマンドである. したがって,Windowsに対応させるならば,次のように書き直すのがよい.

if (has('win95') || has('win16') || has('win32') ||  has('win64')) && executable('taskkill')
  command! -bar Suicide  call system('taskkill /pid ' . getpid())
elseif executable('kill')
  command! -bar Suicide  call system('kill -KILL '. getpid())
endif

この自殺コマンドはVimを強制的に終了させるため,終了時の後始末が行われない. そのため,カーソルの形状や文字色等が元に戻らないだろう. あくまでジョークコマンドなので,利用価値は無いといえる.

参考

Vimのmessages履歴について

Vim:messages とコマンドを実行すると, :echomsg:echoerr から出力されたメッセージ履歴を閲覧することができる. :help :messages によると,tinyバージョンであれば,過去のメッセージを20行,それ以外だと200行閲覧することができる. しかし,最新のメッセージのみを閲覧したい場合,最初の方のメッセージを邪魔に感じることがある.

ターミナルの clear コマンドのように,過去のメッセージを消去するには,

function! s:clear_messages() abort
  for i in range(201)
    echomsg ''
  endfor
endfunction
command! -bar Clear  call s:clear_messages()

とコマンドを定義し, :Clear とコマンドを実行するとよい. 201回ではなく200回空文字列を出力した場合,1行分クリアできないことがあるので注意しよう.

ちなみに,

function! s:clear_message() abort
  for i in range(201)
    echomsg ' '
  endfor
endfunction
command! -bar Clear  call s:clear_message()

のように,201回半角スペースを出力した場合,201行の半角のスペースが連続してしまい,視覚的な負担は減るが,縦長の履歴になり,煩わしいことに変わりはない.

これだけでは芸が無い(この記事を書いてから,全く同じ内容の記事があることに気がついた)ので, head コマンドや tail コマンドのように,最初からxx行,最後のxx行のmessages履歴を表示できるコマンドを定義しよう.

function! s:redir(cmd) abort
  let [verbose, verbosefile] = [&verbose, &verbosefile]
  set verbose=0 verbosefile=
  redir => str
    execute 'silent!' a:cmd
  redir END
  let [&verbose, &verbosefile] = [verbose, verbosefile]
  return str
endfunction

function! s:echo(use_echomsg, lines) abort
  if a:use_echomsg
    for line in a:lines
      echomsg line
    endfor
  else
    for line in a:lines
      echo line
    endfor
  endif
endfunction

function! s:messages_head(has_bang, ...) abort
  let n = a:0 > 0 ? a:1 : 10
  let lines = filter(split(s:redir('messages'), "\n"), 'v:val !=# ""')[: n]
  call s:echo(a:has_bang, lines)
endfunction
command! -bar -bang -nargs=? MessagesHead  call s:messages_head(<bang>0, <f-args>)

function! s:messages_tail(has_bang, ...) abort
  let n = a:0 > 0 ? a:1 : 10
  let lines = filter(split(s:redir('messages'), "\n"), 'v:val !=# ""')
  if n > len(lines)
    let n = len(lines)
  endif
  let lines = lines[len(lines) - n :]
  call s:echo(a:has_bang, lines)
endfunction
command! -bar -bang -nargs=? MessagesTail  call s:messages_tail(<bang>0, <f-args>)

:MessagesHead 5:MessagesTail 20 のように,引数に指定した行数だけ, :messages の履歴を表示する. 引数に指定しなかった場合は,デフォルト値として,10行だけ表示するようになっている. ! を付加してコマンドを実行した場合は, echo ではなく, echomsg で出力する.

以下はおまけの話だ.

もし,第二引数にハイライトグループを指定したいなら,次のようにするとよい.

function! s:redir(cmd) abort
  let [verbose, verbosefile] = [&verbose, &verbosefile]
  set verbose=0 verbosefile=
  redir => str
    execute 'silent!' a:cmd
  redir END
  let [&verbose, &verbosefile] = [verbose, verbosefile]
  return str
endfunction

function! s:echo(use_echomsg, lines) abort
  if a:use_echomsg
    for line in a:lines
      echomsg line
    endfor
  else
    for line in a:lines
      echo line
    endfor
  endif
endfunction

" ハイライトグループを補完する関数
" 2つ目の引数でないと,補完候補を返却しない
" -complete=highlight では2つ目の引数のみ補完という動作が実現できないので,エミュレート関数を用意した
function! s:complete_messages(arglead, cmdline, cursorpos)
  let nargs = a:cmdline ==# '' ? 1 : len(split(split(a:cmdline, '[^\\]\zs|')[-1], '\s\+'))
  if nargs == 2 || (nargs == 3 && a:arglead !=# '')
    let _arglead = tolower(a:arglead)
    return sort(filter(map(split(s:redir('highlight'), "\n"), 'split(v:val, "\\s\\+")[0]'),
          \ '!stridx(tolower(v:val), _arglead)'))
  else
    return []
  endif
endfunction

function! s:messages_head(has_bang, ...)
  let n = a:0 > 0 ? a:1 : 10
  let lines = filter(split(s:redir('messages'), "\n"), 'v:val !=# ""')[: n]
  if a:0 > 1
    execute 'echohl' a:2
  endif
  call s:echo(a:has_bang, lines)
  if a:0 > 1
    echohl None
  endif
endfunction
command! -bar -bang -nargs=+ -complete=customlist,s:complete_messages MessagesHead
      \ call s:messages_head(<bang>0, <f-args>)

function! s:messages_tail(has_bang, ...)
  let n = a:0 > 0 ? a:1 : 10
  let lines = filter(split(s:redir('messages'), "\n"), 'v:val !=# ""')
  if n > len(lines)
    let n = len(lines)
  endif
  if a:0 > 1
    execute 'echohl' a:2
  endif
  let lines = lines[len(lines) - n :]
  call s:echo(a:has_bang, lines)
  if a:0 > 1
    echohl None
  endif
endfunction
command! -bar -bang -nargs=+ -complete=customlist,s:complete_messages MessagesTail
      \ call s:messages_tail(<bang>0, <f-args>)

2つ目の引数の補完時のみ,ハイライトグループの補完ができる. すなわち,

:MessagesHead 15 E<Tab>

:MessagesTail 25 <Tab>

<Tab> を入力すると,ハイライトグループが補完される.

参考

Vimのサーバー機能でタイマーを実現する

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-vimosyo-manga/vim-sugarpot は,ややホビー用途のプラグインのように思えたが,このプラグインは実用的だと感じた.