成果
背景
最近、 vim
で :terminal
使って生きてる。
bashrc の最後で
[[ -z "$VIM_TERMINAL" ]] && vim && exit
とか書いちゃうレベル。
ところで、 :terminal
の中で hub pull-request
とかうかつに使うと vim on :terminal on vim になってキモイ。
なんとかならんか。
作った
安定の英語わからないマン。
vimで$EDITOR環境変数をいじることで、:terminal起動すると、外のvimを使うようになるプラグイン。
残課題
- 複数ファイルを編集するような場合ってどうなってるんだっけ?
$EDITOR file1 file2 file3...
みたいな感じなのかな。いつを終了とするかとか考えると、結構に難しそう
終了コード設定してない
編集の成否(とは?)によって終了コードが設定されるらしく、
EDITORを使う側は終了コードで判定している(git commit
など?)場合もあるらしい
作るまでの四苦八苦など
Tapi
使ってやってみる
mattn神が書かれてる記事のとおりにやりゃあ、:terminal側から外のvimに編集をぶん投げられる。
$ echo hello! > hoge.txt
$ echo -ne "\033]51;[\"drop\",\"hoge.txt\"]\07"
こうすると、実際外のvimで hoge.txt
の編集が開始される。
なら、そういう関数作りゃ良いのではないか。
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
しておく
#!/bin/sh
echo -ne "\033]51;[\"drop\",\"$1\"]\07"
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秒だけ待った。
function! Tapi_EditFile(bufnum, arglist)
if len(a:arglist) == 1
" edit a:arglist[0]
sleep 5000m
endif
endfunction
#!/bin/sh
echo -ne "\033]51;[\"call\", \"Tapi_EditFile\", [\"$1\"]]\07"
ナントまァ。ちゃんと5秒待ってくれた。
(あとでキャプチャここに貼る)
・・・どういう実装なんだ。奥深いぞvim。
編集開始→保存されるのを待つ→保存されたら自動でquit
quit
Vim Script歴やや浅めなのでここが一番つらそう。
そもそも 保存されるのを待つ のがナンセンスではないか。
Tapi_EditFile
の中で保存されるのを待ってしまったら、vim自体が動きようがなくなる。
つまるところ、 tvim
の方で編集終了を検知して終了しないと行けない。
Shell側で待つ実装を考える
どうするのがいいんだろうか。
-
tvim
からTapi_EditFile
でファイルを渡す - バッファを閉じる時にVim側で なにかする
-
tvim
側で なにか を検出して処理を終了する
のが基本方針だろう。では なにか に適切な処理はなんだろう
term_sendkeys
tvim
側で
#!/bin/sh
echo "\033]51;[\"call\", \"Tapi_EditFile\", [\"$1\"]]\007"
while read response; do
if [[ "${response}" == "close" ]]; then
break
fi
done
として、 Tapi_EditFile
では
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すればいいんだった
#!/bin/sh
echo "\033]51;[\"call\", \"Tapi_EditFile\", [\"$1\"]]\007"
while read _dummy; do
:
done
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とかもナシで行けそう
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
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
にも対応しておこう)
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
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}"