LoginSignup
5
4

More than 3 years have passed since last update.

vim on vim を本気で回避する

Last updated at Posted at 2019-03-23

成果

背景

最近、 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 の編集が開始される。

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

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}"
5
4
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4