Vimをtmuxの代わりに使ってみた

# はじめに
ぼくは普段開発する時、必ずtmuxとVimを併用しています。tmuxは本当に便利で、画面分割したり、セッションを繰り替えたりながら作業をするのに必須と言ってよいほどです。

しかしVim使いのぼくはやはりVimだけで生活したいので、tmuxをやめてVimだけでtmuxの機能を一部実現してみました。
意外となんとかなったので、そのやり方を解説していきます。

[![asciicast](https://asciinema.org/a/260659.svg)](https://asciinema.org/a/260659)

# 仕組みの概要
tmuxの画面分割してターミナルを開くのは、Vimの画面分割とターミナルを組み合わせて実現できます。
例えば`:vert terminal ++close bash`で縦二分割してターミナルを開くことができます。

tmuxのセッションに関しては、Vimのセッション機能を使用します。
セッションについては[こちらの記事](https://gorilla.netlify.com/articles/20190620-vim-session-plugin.html)を参照してください。

ただ、Vimではターミナルウィンドウの位置、サイズを復元できますが、状態までは復元できません。
仕様上どうしてもここは完全代用が難しいので、ここは無理せず使い方でカバーすればよいです。

# 画面分割
[![asciicast](https://asciinema.org/a/260715.svg)](https://asciinema.org/a/260715)

tmux使用していた時は`CTRL-S + \`で縦、`CTRL-S + -`で横で画面分割、`CTRL-S + c`でタブを開くようにしていました。
Vimでは`OpenTermianl`コマンドを定義して、そのコマンド内でバッファを作成して、バッファでターミナルを開くようにしています。
コマンドをキーマッピングすれば、tmuxと同じことができます。

ソースは次になります。

```vim
" ターミナルを開く
" a:1 new or vnew or tabnew(default is new)
" a:2 path (default is current)
" a:3 shell (default is &shell)
function! s:open_terminal(...) abort
	let open_type = 'new'
	let shell = &shell
	let path = getcwd()

	if a:0 > 0 && a:0 !=# ''
		let open_type = a:1
	endif
	if a:0 > 1 && a:2 !=# ''
		let path = a:2
	endif
	if a:0 > 2 && a:3 !=# ''
		let shell = a:3
	endif
	if open_type ==# 'new'
		let open_type = 'bo ' .. open_type
	endif

	exe printf('%s | lcd %s', open_type, path)
	exe printf('term ++curwin ++close %s', shell)
	exe 'call term_setrestore("%", printf("++close bash -c \"cd %s && bash\"", getcwd()))'
endfunction

command! -nargs=* OpenTerminal call s:open_terminal(<f-args>)

" ターミナルを開く
noremap <silent> <C-s>\ :OpenTerminal vnew<CR>
noremap <silent> <C-s>- :OpenTerminal<CR>
noremap <silent> <C-s>^ :OpenTerminal tabnew<CR>
tnoremap <silent> <C-s>\ <C-w>:OpenTerminal vnew<CR>
tnoremap <silent> <C-s>- <C-w>:OpenTerminal<CR>
tnoremap <silent> <C-s>^ <C-w>:OpenTerminal tabnew<CR>
```

`noremap`はノーマルモード、`tnoremap`はターミナルで動作するマッピングです。

ターミナルのオプションですが、`++close`はターミナルを終了する時にバッファを閉じる、`++curwin`は現在のバッファでターミナルを開くという動きになります。

ターミナルを開いた後に実行している`term_setrestore`はかなり重要で、これはセッションでターミナルを復元する時に実行するコマンドです。

デフォルト、ターミナルを復元する時はカレントディレクトリになってしまいます。
なので、セッション保存時のディレクトリで復元するように設定する必要があります。

上記のコードを`vimrc`に追記すれば、そのまま動くはずです。

# セッション保存/復元
セッション機能を使用することで、画面の状態をそのまま保持できます。
なので、基本的セッションを保存、起動時or起動後にセッションを読み込むことで復元できます。

しかし、セッションを使う上でいくつか注意する必要があります。

- なるべくプレーンの状態でセッションを復元する  
大体復元するタイミングはVimを再起動した後だと思うので、それほど意識する必要は無いですが、
例えばターミナルを開いた状態でセッションを復元しようとするとエラーが出ます。
なので、基本的に起動後にセッションを読み込むようにします。

- セッションを保存する時のオプションに`globals`を外す  
セッションの保存対象を設定する `sessionoptions`というオプションがありますが、
デフォルではフローバル変数を復元します。それだとvimrcの一部の設定などが復元してしまいます。
保存時のvimrcの状態を戻すよりも、最新vimrcを適用したいケースが多いので、ここではオプションから外します。

ちなみに、ぼくの設定は次になっています。

```vim
set sessionoptions=blank,buffers,curdir,folds,help,tabpages,winsize,terminal
```

## タブページの移動
Vimのタブをプロジェクトごとで分けて使うため、タブ移動＝プロジェクト移動にしています。
デフォルト`gt`と`gT`でタブを切り替えられますが、個人的に使いづらいので、次の様にしています。

このキーマップはtmuxの設定と同じにしています。

```vim
nnoremap <C-s>n gt
nnoremap <C-s>p gT
tnoremap <C-s>p <C-w>:tabprevious<CR>
tnoremap <C-s>n <C-w>:tabnext<CR>
```

# ghqとfzf.vimとと組み合わせる

## リポジトリを開く
[![asciicast](https://asciinema.org/a/260717.svg)](https://asciinema.org/a/260717):

基本的にVimを開くときは何かしらプロジェクトに移動して作業する時なので、
[ghq](https://github.com/motemen/ghq)と[fzf.vim](https://github.com/junegunn/fzf.vim)を組み合わせてプロジェクトに簡単に移動した後にターミナルを開く`Repo`コマンドを用意しました。

```vim
function! s:cd_repo(shell, repo) abort
	exe 'lcd' trim(system('ghq root')) .. '/' .. a:repo
	call s:open_terminal('new', '', a:shell)
	exe 'wincmd k'
	pwd
endfunction

function! s:repo(multi, cb) abort
	if executable('ghq') && exists('*fzf#run()') && executable('fzf')
		call fzf#run({
					\ 'source': systemlist('ghq list'),
					\ 'sink': a:cb,
					\ 'options': a:multi,
					\ 'down': '40%'},
					\ )
	else
		echo "doesn't installed ghq or fzf.vim(require fzf)"
	endif
endfunction

command! Repo call s:repo('+m', function('s:cd_repo', [&shell]))
```

`ghq list`でリポジトリ一覧を取得して、`fzf.vim`であいまい検索できるようにしています。

## リポジトリを複数開く
[![asciicast](https://asciinema.org/a/260719.svg)](https://asciinema.org/a/260719)

ときに複数のプロジェクトを同時に開きたいことがあります。
何度も`:Repo`を実行しても良いですが、面倒です。
なので、`NewTab`コマンドというのを用意しました。

```vim
function! s:open_tabs(shell, repo) abort
	exe printf('tabnew | lcd %s/%s', trim(system('ghq root')), a:repo)
	exe printf('bo term ++rows=20 ++close %s', a:shell)
	exe 'call term_setrestore("%", printf("++close bash -c \"cd %s && bash\"", getcwd()))'
	exe 'wincmd k'
endfunction

command! NewTab call s:repo('-m', function('s:open_tabs', [&shell]))
```

`fzf.vim`は`-m`オプション使うと複数の項目を選択することができるので、それを使用します。
ここで1点注意ですが`fzf.vim`のcallbackでは`&shell`が`sh`になってしまうので、呼び出し元の`shell`を渡す必要があります。

# 残作業
## タブのラベル
タブのラベルを自由に決めれるようにしたいのです、ラベルの仕組みが思ったよりもややこしく途中で諦めました。
ラベルを簡単に変えれれば一番良いですが、なくてもそこまで問題ないです。

## 画面最大化のトグル
tmuxでは`prefix+z`で画面の最大化のトグルができます。Vimでもそれをやろうと思えば実現はできると思いますが、まだ試せていないです。
ターミナルがある状態で`<C-w>o`でターミナルが残るので、その問題が解決できればという感じです。

# まとめ
ターミナルとセッションの機能で、なんとかtmuxっぽいことはできました。
今の所それほど困ってはいないのですが、やはりtmuxはすごいなと改めて思いました。

ただ、Vimだけでもここまでできるこを知れたので大きいですね。
興味ある方はぜひvimrcに追加して試してみてください。