koturnの日記

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

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

Vimのrange指定した関数を呼び出したときにハマッたこと

.vimrcに昔記述した,カーソルを移動せず,ファイルの全ての行末のホワイトスペースを消去するコマンドを,範囲指定できるように書き直したときにハマった話をする. 以下のコマンドを,

function! s:delete_trailing_whitespace() abort
  let cursor = getcurpos()
  silent keepjumps %s/\s\+$//ge
  call histdel('search', -1)
  call setpos('.', cursor)
endfunction
command! -bar DeleteTrailingWhitespace  call s:delete_trailing_whitespace()

関数のrange識別を用いて,次のように書き直した.

function! s:delete_trailing_whitespace() abort range
  let cursor = getcurpos()
  execute 'silent keepjumps' a:firstline ',' a:lastline 's/\s\+$//ge'
  call histdel('search', -1)
  call setpos('.', cursor)
endfunction
command! -bar -range=% DeleteTrailingWhitespace  <line1>,<line2>call s:delete_trailing_whitespace()

しかし,このコマンドを :DeleteTrailingWhitespace のように実行する(範囲は1行目から最終行)と,カーソルがファイルの1行目に移動してしまう. (-range=% は,範囲指定をしないときは,1行目から最終行が指定されたことになる) どうやら,range識別をつけた関数内における getcurpos() で得られるのは,指定された範囲の最初の位置であるらしく,関数を呼び出した時点でのカーソル位置が得られるわけではない.

そのため,次のように,関数の引数として範囲を渡すように書き換えた. (引数名に firstlinelastline を用いることはできない)

function! s:delete_trailing_whitespace(line1, line2) abort
  let cursor = getcurpos()
  execute 'silent keepjumps' a:line1 ',' a:line2 's/\s\+$//ge'
  call histdel('search', -1)
  call setpos('.', cursor)
endfunction
command! -bar -range=% DeleteTrailingWhitespace  call s:delete_trailing_whitespace(<line1>, <line2>)

これならば,関数内で呼び出された getcurpos() で得られるのは,関数を呼び出した時点でのカーソル位置である.

今までrange識別を用いた関数を書いたことがなかったので,このような挙動をすることがわかっていなかった. もっとスマートな解決方法があるのかもしれない.

Vimで編集中のファイルを(ほぼ)2度と編集できなくする

最初に言っておくが,この記事は単なるジョーク記事である.

Vimには暗号化を行う機能がある. :help 'cryptmethod':help :X:help 'key' あたりを参照してもらえばよくわかるはずだ.

まず,カレントバッファにて, :X というコマンドを実行してほしい. 暗号化に用いるキーを聞かれるはずだ, キーを入力し,確認のためにもう一度キーを入力すると,編集中のファイルは暗号化される状態になる. この状態で保存すると,そのファイルは暗号化された状態で保存される. 再度そのファイルをVimで開こうとすると,キー入力が求められるはずだ. 誤ったキーを入力すると,バイナリファイルのような状態になると思う. また,Vim以外で暗号化されたファイルを開くと,当然うまく開くことはできない.

f:id:koturn:20150728063451g:plain

この暗号化に用いる方式は,Vim7.4.788の段階で,zip, blowfish, blowfish2 の3種類から選択でき,それをオプションcryptmethod に指定する. 暗号化方式は,Vimのアップデートに伴い,新たに追加されていくので,注目しておく必要がある. 古い暗号化方式を指定すると,コマンド :X を用いるときにワーニングメッセージが出る. 新しいVimで利用できる暗号化方式を用いた場合,その暗号化方式を利用できない古いVimでは E821 エラーが発生するらしい.

f:id:koturn:20150728063511p:plain

オプション cryptmethod の既定値は一番古い暗号化方式である zip であるため,最新の暗号化方式を用いたい場合,以下を.vimrcに記述しよう.

if v:version > 704 || v:version == 704 && has('patch399')
  set cryptmethod=blowfish2
elseif v:version >= 703
  set cryptmethod=blowfish
else
  set cryptmethod=zip
endif

なお,暗号化に用いるキーは,オプション key に指定することで設定できるが,手動で設定すると盗み見られるため,危険である(このことはヘルプにも書かれているはずだ). また,オプション key の内容は取得できない. echo &key としても,キーが設定されていないならば空文字列,キーが設定されていれば '*****' が返却されるだけである.

敢えて,このオプション key の値を設定すると,コマンド :X を実行した状態と同じになる. つまり,その状態で保存すれば,編集中のファイルは暗号化された状態で保存される.

暗号化されたファイルを再度開き,編集して保存したとしても,暗号化状態は解除されない. 暗号化せずに保存したいときは,コマンド :X で何も入力しないで(<CR>のみを入力して)おく,もしくは,オプション key の値を空にすればいい( set key= ).

さて,この暗号化機能を利用すれば,編集中のファイルを2度と開けなくすることが可能になるのではないか?と考えた. 暗号化に用いるキーに,使用者にとってもわからないランダムなものを設定するだけの単純な話だ. 以下を.vimrcに記述し, :DestroyFile! とコマンドを実行すれば,編集中のファイルはおそらく2度と開けなくなるだろう. 危険なコマンドなので, ! を付加しなければ,実行できないようにしてある. このコマンドを大事なファイルで実行しないように注意しよう.

if has('cryptv')
  function! s:destroy_file(has_bang) abort
    if !filereadable(expand('%:p'))
      echoerr 'Current buffer is not write out to file'
      return
    endif
    if !a:has_bang
      echoerr "Must to add '!' to destroy this file"
      return
    endif
    let cursor = getpos('.')
    normal! ggg~Gg?G
    call setpos('.', cursor)
    let &key = sha256(reltimestr(reltime()))
    write
    bwipeout
  endfunction
  command! -bar -bang DestroyFile  call s:destroy_file(<bang>0)
endif

暗号化に用いるキーは, sha256() に現在時間の文字列を与えることで生成する. これをキーに設定し,保存して,編集中のファイルをバッファから消去するだけである. 一応,暗号化前にバッファ全体に対して,アルファベットの大文字と小文字を入れ替えた後,rot13を行うようにしてある( normal! ggg~Gg?G の部分)が,これは大した意味をもたないだろう. 英文字以外に対しては意味のない操作の上,小文字大文字の入れ替えとrot13は暗号化としては貧弱すぎる. ジョークの一環として,ついでにしておいただけだ.

VimでJsonを整形したい

jqという非常に便利なコマンドラインツールがある. これは,jsonの整形を行うものだ. jqコマンド自体については,公式のマニュアルや以下の記事を参考にすればよくわかるだろう.

この便利なjqコマンドをVim上で利用したい. そのように考えた人は既におり,以下のいくつかの記事が参考になる.

これらの記事に書いてあるように,

:%!jq .

と外部コマンドを実行したり,

if executable('jq')
  function! s:jq(...)
    execute '%!jq' (a:0 == 0 ? '.' : a:1)
  endfunction
  command! -bar -nargs=? Jq  call s:jq(<f-args>)
endif

のようなコマンドを定義し,:Jq とコマンドを実行すればよい.

しかし,Json自体に誤りがあった場合は,Jsonを表示しているカレントバッファからJsonが消え,エラーメッセージに置き換えられてしまう. いちいちundoするのも面倒だ. そこで,エラーがある場合は,別のバッファにエラーメッセージを表示し,カレントバッファに変更を加えないようにしたい. 前回書いた記事:VimでC言語のソースコードを整形したいを模倣し,コマンドを改良する.

if executable('jq')
  function! s:jq(has_bang, ...) abort range
    execute 'silent' a:firstline ',' a:lastline '!jq' string(a:0 == 0 ? '.' : a:1)
    if !v:shell_error || a:has_bang
      return
    endif
    let error_lines = filter(getline('1', '$'), 'v:val =~# "^parse error: "')
    " 範囲指定している場合のために,行番号を置き換える
    let error_lines = map(error_lines, 'substitute(v:val, "line \\zs\\(\\d\\+\\)\\ze,", "\\=(submatch(1) + a:firstline - 1)", "")')
    let winheight = len(error_lines) > 10 ? 10 : len(error_lines)
    " カレントバッファがエラーメッセージになっているので,元に戻す
    undo
    " カレントバッファの下に新たにウィンドウを作り,エラーメッセージを表示するバッファを作成する
    execute 'botright' winheight 'new'
    setlocal nobuflisted bufhidden=unload buftype=nofile
    call setline(1, error_lines)
    " エラーメッセージ用バッファのundo履歴を削除(エラーメッセージをundoで消去しないため)
    let save_undolevels = &l:undolevels
    setlocal undolevels=-1
    execute "normal! a \<BS>\<Esc>"
    setlocal nomodified
    let &l:undolevels = save_undolevels
    " エラーメッセージ用バッファは読み取り専用にしておく
    setlocal readonly
  endfunction
  command! -bar -bang -range=% -nargs=? Jq  <line1>,<line2>call s:jq(<bang>0, <f-args>)
endif

前回の記事とほとんど内容は変わらない. フィルタリングに用いるコマンドのエラーを判別し,エラー用の処理を加えることで,いくつかの記事で紹介されているjqを用いるコマンドより便利になったというだけだ.

参考