vim on vim を本気で回避する

## 成果

<a href="https://github.com/kyoh86/vim-editerm"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/kyoh86/vim-editerm.png" width="460px"></a>

## 背景

最近、 `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を使うようになるプラグイン。

## 残課題

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

```shell
$EDITOR file1 file2 file3...
```

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

* ~~終了コード設定してない~~

~~編集の成否（とは？）によって終了コードが設定されるらしく、~~
~~EDITORを使う側は終了コードで判定している（`git commit`など？）場合もあるらしい~~

## 作るまでの四苦八苦など

### `Tapi` 使ってやってみる

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

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

```shell-session
$ echo hello! > hoge.txt
$ echo -ne "\033]51;[\"drop\",\"hoge.txt\"]\07"
```

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

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

```zsh:zhsrc
  function tvim() {
    echo -ne "\033]51;[\"drop\",\"$1\"]\07"
  }
  EDITOR=tvim
```

```shell-session
$EDITOR hoge.txt
```

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

```shell-session
$ 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` しておく

```shell:tvim
#!/bin/sh
echo -ne "\033]51;[\"drop\",\"$1\"]\07"
```
```zsh:zhsrc
  EDITOR=tvim
```

これも駄目。

```shell-session
$ 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秒だけ待った。

```vim:vimrc
function! Tapi_EditFile(bufnum, arglist)
  if len(a:arglist) == 1
    " edit a:arglist[0]
    sleep 5000m
  endif
endfunction
```

```shell: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` 側で

```shell:tvim
#!/bin/sh

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

として、 `Tapi_EditFile` では

```vim: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すればいいんだった

```shell:tvim
#!/bin/sh

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

```vim: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とかもナシで行けそう

```shell: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
```

```vim: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`にも対応しておこう）

```shell: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
```

```vim: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はいらない。

だのでこう。

```diff
 function! s:open_buffer(filename)
   execute('new '.a:filename)
+  setlocal bufhidden=wipe
   return bufnr('%')
 endfunction
```
```diff
 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するのではなく、終了コードを書き込むことで引き渡している。

```diff
 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
```

```diff
@@ -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}"
```