Edited at

vim on vim を本気で回避する


成果


背景

最近、 vim:terminal 使って生きてる。

bashrc の最後で

[[ -z "$VIM_TERMINAL" ]] && vim && exit

とか書いちゃうレベル。

ところで、 :terminal の中で hub pull-request とかうかつに使うと vim on :terminal on vim になってキモイ。

なんとかならんか。


作った

https://github.com/kyoh86/vim-editerm

安定の英語わからないマン。

vimで$EDITOR環境変数をいじることで、:terminal起動すると、外のvimを使うようになるプラグイン。


残課題


  • 複数ファイルを編集するような場合ってどうなってるんだっけ?

$EDITOR file1 file2 file3...

みたいな感じなのかな。いつを終了とするかとか考えると、結構に難しそう


  • 終了コード設定してない

編集の成否(とは?)によって終了コードが設定されるらしく、

EDITORを使う側は終了コードで判定している(git commitなど?)場合もあるらしい


作るまでの四苦八苦など


Tapi 使ってやってみる

mattn神が書かれてる記事のとおりにやりゃあ、:terminal側から外のvimに編集をぶん投げられる。

https://qiita.com/mattn/items/e99e5dc7c4054ba25e7d

$ echo hello! > hoge.txt

$ echo -ne "\033]51;[\"drop\",\"hoge.txt\"]\07"

こうすると、実際外のvimで hoge.txt の編集が開始される。

なら、そういう関数作りゃ良いのではないか。


zhsrc

  function tvim() {

echo -ne "\033]51;[\"drop\",\"$1\"]\07"
}
EDITOR=tvim

$EDITOR hoge.txt

ちゃんと編集が開始される。ところがどっこい。

$ hub pull-request

error using text editor for pull request message

$ git commit -a
hint: Waiting for your editor to close the file... error: cannot run tvim: No such file or directory
error: unable to start editor 'tvim'
Please supply the message using either -m or -F option.

hub とか git は、愚直に$EDITORを呼ぶんじゃなくて、executableかどうかチェックしてるっぽい

2019/06/03追記:executableかどうかチェックするも何も、そもそもfunctionはシェルの持ち物なんで、子プロセスから呼び出せないのは道理である。

阿呆の所業であった。


ちゃんとexecutable作る

じゃあexecutableになってりゃいいんだろということで、

functionの中身をシェルスクリプトファイルとしてパス通った場所において chmod +x しておく


tvim

#!/bin/sh

echo -ne "\033]51;[\"drop\",\"$1\"]\07"


zhsrc

  EDITOR=tvim


これも駄目。

$ hub pull-request

error using text editor for pull request message

$ git commit -a
hint: Waiting for your editor to close the file... -ne
Aborting commit due to empty commit message.

hub の方は、沈黙した後エラーメッセージを吐いて終わった。

git の方は、編集画面が開くところまで入ったが、開くだけで tvim は終了してしまう(中身は実態ただのechoだしそりゃそうだ)ので、コミットメッセージがねえぞこらと怒られて終わる。


drop が駄目なのでは?と疑ってみる

...ちょっと考えにくいけど、dropではなくcallを使えば処理が同期されたりしないだろうか?

編集をはじめて、終了するのを待つvimscriptを考えるのが少しダルいので、

vim側はsleepで5秒だけ待った。


vimrc

function! Tapi_EditFile(bufnum, arglist)

if len(a:arglist) == 1
" edit a:arglist[0]
sleep 5000m
endif
endfunction


tvim

#!/bin/sh

echo -ne "\033]51;[\"call\", \"Tapi_EditFile\", [\"$1\"]]\07"


ナントまァ。ちゃんと5秒待ってくれた。

(あとでキャプチャここに貼る)

・・・どういう実装なんだ。奥深いぞvim。


編集開始→保存されるのを待つ→保存されたら自動でquit

Vim Script歴やや浅めなのでここが一番つらそう。

そもそも 保存されるのを待つ のがナンセンスではないか。

Tapi_EditFile の中で保存されるのを待ってしまったら、vim自体が動きようがなくなる。

つまるところ、 tvim の方で編集終了を検知して終了しないと行けない。


Shell側で待つ実装を考える

どうするのがいいんだろうか。



  1. tvim からTapi_EditFile でファイルを渡す

  2. バッファを閉じる時にVim側で なにかする


  3. tvim 側で なにか を検出して処理を終了する

のが基本方針だろう。では なにか に適切な処理はなんだろう


term_sendkeys

tvim 側で


tvim

#!/bin/sh

echo "\033]51;[\"call\", \"Tapi_EditFile\", [\"$1\"]]\007"
while read response; do
if
[[ "${response}" == "close" ]]; then
break
fi
done


として、 Tapi_EditFile では


vimrc


let s:remotes={}
function! s:leave_remote()
let l:bn = bufnr('%') " 発火元のバッファ
let l:tn = s:remotes[l:bn] " ファイルを開くもととなったTerminalバッファ
call term_sendkeys(l:tn, "close\<CR>")
unlet s:remotes[l:bn]
endfunction

function! Tapi_EditFile(bufnum, arglist)
if len(a:arglist) == 1
execute('new '.a:arglist[0])
let s:remotes[bufnr('%')] = a:bufnum
autocmd BufLeave <buffer> :call <SID>leave_remote()
endif
endfunction


とかやっちゃう。


closeて。

冷静に考えると、別に close なんて文字列は要らない。

while read で 無限に待って、vim側から<C-c>でSIGINTすればいいんだった


tvim

#!/bin/sh

echo "\033]51;[\"call\", \"Tapi_EditFile\", [\"$1\"]]\007"
while read _dummy; do
:
done



vimrc


let s:remotes={}
function! s:leave_remote()
let l:bn = bufnr('%') " 発火元のバッファ
let l:tn = s:remotes[l:bn] " ファイルを開くもととなったTerminalバッファ
call term_sendkeys(l:tn, "\<C-c>")
unlet s:remotes[l:bn]
endfunction

function! Tapi_EditFile(bufnum, arglist)
if len(a:arglist) == 1
execute('new '.a:arglist[0])
let s:remotes[bufnr('%')] = a:bufnum
autocmd BufLeave <buffer> :call <SID>leave_remote()
endif
endfunction


だがこれ、 tvim を呼び出した後、普通に<C-c>でキャンセルしてから、

vim側で開いたファイルをcloseすると、また<C-c>が飛んでくる。これはキモイ。

うっかり大事な処理動かしたあとで発火されたりしたら僕も自然発火する

term_sendkeys 自体、こういう目的だと使いにくそうだ。


file wait

なら、ロックファイルよろしく、編集始めるときにファイルを作っておいて、

そいつが消えるのを待てばいいのではないか。



  • tvim 側でtempfileを生成して、ファイルが消されるのを待つ。

  • vim側でLeaveするときにそのファイルを削除する

inotifywait があったら使うとSleepとかもナシで行けそう


tvim

tmpfile=`mktemp`

echo "\033]51;[\"call\", \"Tapi_EditFile\", [\"${tmpfile}\", \"$1\"]]\007"

if which inotifywait >/dev/null 2>&1; then
inotifywait -e delete_self ${tmpfile}
else
while
true; do
if
[[ ! -f ${tmpfile} ]]; then
break
fi
sleep 0.5
done
fi



vimrc


let s:remotes={}
function! s:leave_remote()
let l:bn = bufnr('%') " 発火元のバッファ
let l:tf = s:remotes[l:bn] " 編集終了を待つtmpfile
call delete(l:tf)
unlet s:remotes[l:bn]
endfunction

function! Tapi_EditFile(bufnum, arglist)
if len(a:arglist) == 2
let l:tmpfile = a:arglist[0]
execute('new '.a:arglist[1])
let s:remotes[bufnr('%')] = l:tmpfile
autocmd BufLeave <buffer> :call <SID>leave_remote()
endif
endfunction



微調整



  • BufLeave だと、うっかりウインドウ切り替えた拍子に発火して編集終了してしまうのだった。


  • :q で閉じると、次同じファイル開いたときにautocmdが重複するので消しておく(autocmd! * <buffer>)


    • だのでGroupもいるよね



  • Macではinotify系使えないかも(fswatchにも対応しておこう)


tvim

tmpfile=`mktemp`

echo "\033]51;[\"call\", \"Tapi_EditFile\", [\"${tmpfile}\", \"$1\"]]\007"

if which inotifywait >/dev/null; then
inotifywait -e delete_self ${tmpfile}
elif which fswatch >/dev/null; then
fswatch --event 8 -1 -0 --format '' ${tmpfile}
else
while
true; do
if
[[ ! -f ${tmpfile} ]]; then
break
fi
sleep 1
done
fi



vimrc

let s:remotes={}

function! s:leave_remote()
let l:bn = expand('<abuf>')+0 " 発火元のバッファ
let l:tf = s:remotes[l:bn] " 編集終了を待つtmpfile
call delete(l:tf)
unlet s:remotes[l:bn]
endfunction

function! Tapi_EditFile(bufnum, arglist)
if len(a:arglist) == 2
let l:tmpfile = a:arglist[0]
execute('new '.a:arglist[1])
let s:remotes[bufnr('%')] = l:tmpfile
augroup EDITOR_VIM
autocmd! * <buffer>
autocmd BufUnload <buffer> :call <SID>leave_remote()
autocmd QuitPre <buffer> :call <SID>leave_remote()
augroup END
endif
endfunction



バッファ残るのキモい

terminal から呼ばれたvimは、感覚的には別のセッション(?)のようなもので、

:q したらバッファから消えてほしいものである。

でそうすると、常にBufUnloadが走るので、QuitPreはいらない。

だのでこう。

 function! s:open_buffer(filename)

execute('new '.a:filename)
+ setlocal bufhidden=wipe
return bufnr('%')
endfunction

 function! Tapi_EditermEditFile(bufnum, arglist)

if len(a:arglist) == 2
let l:locker = a:arglist[0]
let l:bufnum = s:open_buffer(a:arglist[1])
let s:remotes[l:bufnum] = l:locker
augroup EDITERM
autocmd! * <buffer>
autocmd BufUnload <buffer> :call <SID>leave_remote()
- autocmd QuitPre <buffer> :call <SID>leave_remote()
augroup END
endif
endfunction


:cq に対応するには?

git commit などのコマンドは、 $EDITOR を呼んだ後、その終了コードが0ではなかった場合は処理を中断する。

普通に EDITOR=vim の場合は、vimで :cq とすることで、終了コードが 1 になりこれを実現できる。

だけど、今回は別のバッファを開いているだけなので、 :cq すると親のvimごと死んでしまう。それはこまる。

しょうがないので :CQ コマンドを別途用意し、そちらで終了コードを引き渡すようにした。

単純にファイルをdeleteするのではなく、終了コードを書き込むことで引き渡している。

 let s:save_cpo = &cpo

set cpo&vim

let s:remotes={}
+
+function! s:kill_remote()
+ let l:bufnr = bufnr('%')
+ call s:release_remote(l:bufnr, '1')
+ execute(l:bufnr .. 'bwipeout!')
+endfunction
+
function! s:leave_remote()
- " unloading buffer number
- let l:bufnum = expand('<abuf>')+0
+ call s:release_remote(expand('<abuf>')+0, '0')
+endfunction
+
+function! s:release_remote(bufnum, ret)
" lockfile for the buffer
- let l:locker = get(s:remotes, l:bufnum, '')
+ let l:locker = get(s:remotes, a:bufnum, '')

if l:locker != ''
" delete lockfile
- call delete(l:locker)
+ call writefile([a:ret], l:locker)

- unlet s:remotes[l:bufnum]
+ unlet s:remotes[a:bufnum]
endif
endfunction

function! s:open_buffer(filename)
execute('new '.a:filename)
setlocal bufhidden=wipe
+ command -buffer Cq :call <SID>kill_remote()
+ command -buffer CQ :call <SID>kill_remote()
return bufnr('%')
endfunction

@@ -5,14 +5,27 @@ tmpfile=`mktemp`

echo "\033]51;[\"call\", \"Tapi_EditermEditFile\", [\"${tmpfile}\", \"$1\"]]\007"

if which inotifywait >/dev/null; then
- inotifywait -e delete_self ${tmpfile}
+ inotifywait "${tmpfile}"
elif which fswatch >/dev/null; then
- fswatch --event 8 -1 -0 --format '' ${tmpfile}
+ fswatch -1 -0 --format '' "${tmpfile}"
else
while true; do
- if [[ ! -f ${tmpfile} ]]; then
+ if [[ ! -f "${tmpfile}" ]]; then
+ break
+ fi
+ if [[ -n "${tmpfile}" ]]; then
break
fi
sleep 1
done
fi
+
+if [[ -n "${tmpfile}" ]]; then
+ while read line; do
+ if [[ "${line}" == "" ]]; then
+ continue
+ fi
+ exit $((${line}))
+ done < "${tmpfile}"
+fi
+rm "${tmpfile}"