Vim/Neovimの設定ファイル(.vimrc, init.vim)の書き方を0から一人前まで

#本記事の実施環境
* Ubuntu 20.04 LTS 日本語 Remix(AlterLinux上のVirtualBox)
* NVIM v0.4.3

以前私は[Windowsで競プロに必要なNeovim環境を整える](https://qiita.com/KowerKoint/items/89d1952b54dec57a1a07)という記事を書いたが、結局の所Windowsで開発を行うのはかなり不利である。パッケージ管理ツールがまとものに整備されていなかったり、ドライブレターから始まる特殊なパスを使用していたりしていて、開発の上でWindowsを使うと予期せぬ自体がよく発生して大変不便だ。特にVimを使う場合はLinuxを使うべきだ。

私は現在Windows10とAlterLinux(ArchLinuxの派生OS)をデュアルブートして開発には主にAlterを使っているが、Linuxに移ったことでかなり開発環境の構築が楽になった。最近ではWSL2も動作可能になり、Windows上で簡単にLinuxが使えるようになったのでWindowsユーザーの方も**WSL等のLinux環境を開発の中心にすることをおすすめする**。

本記事では私が普段使っているAlterでの設定を参考にしつつ、最もよく使われているLinuxディストリビューションであるUbuntuで動作確認を行いながら書くことにする。

また、Neovimを前提とした書き方で記事が作られているが、本記事で紹介していることはVimもNeovimも変わらないものである。**本家Vimを使用している場合は`~/.config/nvim/init.vim`や`$MYVIMRC`を`~/.vimrc`と読み替えるだけでほぼすべての設定が使えるはずだ。しかし動作確認は行っていないのでできればNeovimを使うことを推奨する。**

Vim、Neovimを使う意義については[なぜ私はVimを使うのか](https://qiita.com/KowerKoint/items/60568ab149c8ca4e60fa)で書いたのでNeoへの以降を考えるきっかけなどにしてほしい。

#init.vimとは
`init.vim`は、Neovim起動時に最初に読まれるスクリプトだ。

Vimにはコマンドがたくさんあり、基本的には`:`でコマンドモードに入ってコマンドを打つことで実行できる。Vimではエディタの見た目や挙動を設定するのもすべてコマンドでできているが、毎回最初にそれをすべて手作業で実行するのは馬鹿らしい作業だ。そこで**最初にすべて実行しても支障のないようなコマンドを`init.vim`に書き連ねる**ことでその手間を省くことができる。さらに`init.vim`には**Vim sciript**という言語が使えるので、条件分岐を使ったりコマンドを新たに追加することもできる。プラグインは`init.vim`に書いているようなスクリプトを別の場所に書いて読み込ませているようなものなので、一から`init.vim`に書くと大変な`init.vim`をまとめて引用しているようなものと言える。

NeoのつかないVimで`.vimrc`というファイルに書いていたものと同じ感じで、書き方も変わらない。

場所はLinuxでは`~/.config/nvim/init.vim`、Windowsでは`C:\Users\[ユーザー名]\AppData\Local\nvim\init.vim`だ。インストール時に自動で作られるわけではないので作っておこう。

```shell
cd ~/.config
mkdir nvim && cd nvim
touch init.vim
```
これで`init.vim`が作られた。一度`init.vim`が作られるとNeovimの起動時に読み込まれて環境変数`$MYVIMRC`にパスが代入されるので、以後はNeovimを開いた状態で`:e $MYVIMRC`を実行すると`init.vim`のフルパスを覚えてなくても簡単に開くことができる。

#本記事で説明すること
* 変数について
* setコマンド
* mapコマンド
* leaderキー
* normalコマンド
* functionの定義とcall
* autocmd

#前提とすること
* Vimの基礎的な知識
* 一般的なプログラミング言語の知識

#変数について
**※変数は簡単な`init.vim`を書くのには必要ないので読み飛ばしても良い**

Vim scriptも他のプログラミング言語と同様に変数を定義することができる。使える型は整数型、小数型、文字列型、リスト型、辞書型であり、だいたいPython等のスクリプト言語と使い方は同じである。しかしVim scriptは変数への代入の書き方に癖がある。

Vim scriptでは`let [変数名] = [初期化する値]`で変数への代入を行う。ここで注意したいのは、`let`はJavaScriptの`var`のような宣言のための識別子ではないということである。Vim scriptでは`let`は変数に代入する**コマンド**であるので、2回目以降も代入するときには必ず`let`を使う。**`let`はコマンドである。**

【例】

```vimscript
let a = 2 "aに2を代入
echo a "echoは出力のコマンド 2が出力される
let a = 3 "aに3を代入
echo a "3が出力される
```

実は上の書き方はあまり明確ではない。Vim scriptの変数はスコープ(有効範囲)を決めることができる。例えば、`let s:script_value = 1`と宣言すると、そのスクリプト内でのみ有効な変数として使うことができる。以下は、変数名の前につけるものとスコープの表である。

|変数の前|スコープ|
|:---:|:---:|
|g:|グローバル|
|l:|ローカル(関数内)|
|s:|ローカル(スクリプト内)|
|b:|ローカル(バッファ内)|
|w:|ローカル(ウィンドウ内)|
|t:|ローカル(タブページ内)|
|a:|関数の引数(後述)|
これを省略すると変数を作成した場所が関数内なら関数内、外ではグローバルで有効な変数になるようだが、自分で変数を定義する際には明示すると安心だ。

#setコマンド
Vimにはオプションと呼ばれる値が存在している。オプションには切り替えオプション、数値オプション、文字列オプションの3つが存在している。setコマンドではオプションを変えたり、表示したりすることができる。

切り替えオプションは`set [オプション名]`でONにでき、数値オプションと文字列オプションは`set [オプション名]=[値]`で変更したり、複数の値をセットするような文字列オプションでは`set [オプション名]+=[値]`で追加したりできる。

また、`init.vim`に書くよりコマンドモードで直接使う時が多いが、`set [オプション名]?`で現在の値を確認できる。

ちなみに一文字しか変わらないが`set`の代わりに`se`でも使える。

また、`setlocal`を使うと、そのバッファのみでオプションを変更することができるが、`init.vim`に素で書くことはないだろう。これに関しては`autocmd`と一緒に説明する。

Vimにはおびただしい量のオプションが存在するので全部は紹介できないが、私が設定しているオプションを以下に載せておく。必要な設定は調べればたいてい出てくるので、「これが足りない」と思ったら各自調べてみてほしい。

```vimscript:init.vim
"行番号を表示(切り替え)
set number
"タブ文字の代わりにスペースを使う(切り替え)
set expandtab
"プログラミング言語に合わせて適切にインデントを自動挿入(切り替え)
set smartindent
"各コマンドやsmartindentで挿入する空白の量(数値)
set shiftwidth=4
"Tabキーで挿入するスペースの数(数値)
set softtabstop=4
"カレントディレクトリを自動で移動
set autochdir
"バッファ内で扱う文字コード(文字列)
set encoding=utf-8
"書き込む文字コード(文字列) : この場合encodingと同じなので省略可
set fileencoding=utf-8
"読み込む文字コード(文字列のリスト) : この場合UTF-8を試し、だめならShift_JIS
set fileencodings=utf-8,cp932
"Vimの無名レジスタとシステムのクリップボードを連携(文字列のリスト) : ダメならxclipをインストールで使えるかも
set clipboard+=unnamed,unnamedplus
"eコマンド等でTabキーを押すとパスを保管する(文字列のリスト) : この場合まず最長一致文字列まで補完し、2回目以降は一つづつ試す
set wildmode=longest,full
```

#mapコマンド
`map`コマンドは、好きな操作を好きなキーに登録する無限の可能性を秘めたコマンドである。`init.vim`でどのような`map`をするかがVimの設定で最も個性が出るところだと思う。基本的な使い方は`map [実際に押すキー] [押したことにするキー]`である。Ctrlキー、Shiftキー、Altキーとの同時押しはそれぞれ`<C-a>`、`<S-a>`、`<A-a>`と書いたり、Enterキーは`<CR>`、Escキーは`<Esc>`と書いたりするので、わからなかったら調べるといい。例えば、

```vimscript:init.vim
map <C-j> G
```
と書くと、Ctrlキーとjキーを同時押ししたときに変わりに`G`(`Shift+g`)を押したことになる(つまり最終行に飛ぶ)。ここで、`g`と`G`は区別することに注意する。もちろん例として作ったこのマップに実用性はないだろう。

`map`コマンドは、ノーマルモードとビジュアルモード、選択モード、オペレータ待機中で有効だが、適応させるモードによってコマンドも少し異なる。どのモードに対応するかは以下の表のようになる。

|コマンド|モード|
|:---:|:---:|
|map|ノーマルモード、ビジュアルモード、選択モード、オペレータ待機中|
|map!|挿入モード、コマンドモード|
|nmap|ノーマルモード|
|imap|挿入モード|
|cmap|コマンドモード|
|vmap|ビジュアルモードと選択モード|
|xmap|ビジュアルモード|
|smap|選択モード|
|omap|オペレータ待機中|
|lmap|挿入モード、コマンドモード、Lang-Arg|
|tmap|ターミナルモード|
知らない状態が入っているかもしれないが、ここでは本質的でないため説明は省略する。

`noremap`コマンドは、他の`map`に上書きされないコマンドである。`map`を使うと、`map`によって仮想的に押されたキーが別の`map`や別のキーバインドに反応してしまうが、`noremap`ではそれを防ぐことができる。また、`nnoremap`がノーマルモードだけを対象にするように、`map`の前につけたアルファベット一文字を`noremap`の前につけることができる。

さらに、`map`などのコマンドには`<expr>`、`<silent>`などの特別な引数をつけることができる。他にもいくつかあるようだが、`<expr>`と`<silent>`くらいしか使わないのでここではその2つに絞って説明する。

`map`の引数に`<expr>`を入れると、マッピング先が式として評価される。つまり`map <expr> [キー] [式]`で、Vim scriptで書いた式を計算した結果の文字列をそのままキーバイドとして打ったことにするということだ。わかりにくいので以下に例を示す。

```vimscript:init.vim
nnoremap <expr> S* ':%s/\<' . expand('<cword>') . '\>/'
```
これは例としては少し複雑であるが、実際に私が使用しているキーマップである。順を追って説明する。
まず、`expand('<cword>')`はカーソル直下の単語を文字列として返す関数である。
次に、`.`は文字列の結合を意味する。多くのプログラミング言語では`+`などが使われているがVim scriptでは`.`である。
よって、例えばカーソルの下に`hoge`という単語がある時、`':%s/\<' . expand('<cword>') . '\>/'`は`':%s/\<hoge\>/'`と展開される。
つまりこのキーバインドは`S*`と打ったときに変わりに`:%s/\<hoge\>/`というようにキーを打ったことにするというものだ。
ちなみに`:%s/置換前/置換後/g`は置換を行うコマンドで、`\<`は単語の先頭、`\>`は単語の末尾にマッチするメタ文字である。
結果的にこのマップは、`S*`を押すとカーソル下の単語と同じ単語を別の単語に置換するためのコマンドを書きかける、というものである。

最後に、`<silent>`を説明する。これはそのマップによる操作ではコマンドラインに実行したコマンドを表示しないというものである。文章だけでは難しいので以下に例を載せる。これも私が実際に使っているマップである。

```vimscript:init.vim
inoremap <silent> <Esc> <Esc>:call system('fcitx-remote -c')<CR>
```
まず、このキーマップの使用目的を説明する。
Vimで日本語を入力したあと`Esc`キーを押してノーマルモードにもどったとき、日本語入力メソッドがONのままでVimキーバインドが打てなくてイライラすることがある。このキーマップは挿入モードから`Esc`を押したときに自動的に入力メソッドをOFFにするためのものである。
まず、`Esc`を送信して通常通りノーマルモードに戻る。
次に入力メソッドをOFFにするためにシステムコマンド(`fcitx-remote -c`)を実行する。関数については後で解説するが、`system()`はシステムコマンドを実行する関数である。
さらに、`call`コマンドは、関数を実行するコマンドであるので、これによって`system('fcitx-remote -c')`という関数をコマンドから実行する。
全体として、ノーマルモードに戻った後入力メソッドを切るということになるのである。
さて、本題であるが、`<silent>`はここでどのような役割を担っているか、もし`<silent>`がなかったら、ノーマルモードに移行するたびに`:call system('fcitx-remote -c')`というメッセージがコマンドラインに残って邪魔である。そこでこのキーマップに`<silent>`をつけておくと、何も表示せずにコマンドを実行してくれるので、通常の`Esc`を使っているかのように実行することができる。

なお`fcitx-remote -c`はUbuntuではデフォルトでは使えないので`sudo apt install fcitx fcitx-mozc`でインストールした後設定で切り替え方法を`fcitx`の変えるか、他の使えるコマンドを利用すると良い。

余談だが、このキーマップはLinux向きである。Windowsでこの機能を実装するには[pepo-le/win-ime-con](https://github.com/pepo-le/win-ime-con.nvim)というプラグインをインストールするのが賢明だ。

`<silent>`は個人的にそれほど重要ではないと思っているので無理に覚える必要はないと思う。

#leaderキー
`map`コマンドについて説明したが、実は多くのキーバインドがVimによって定義されていて、どのキーバインドともかぶらないものを設定するというのは非常に難しい。そこで登場するのが`leader`キーである。`leader`を使うキーバインドはデフォルトでは登録されておらず、好きなように使うことができる。とはいっても本来キーボードに`leader`なんてキーは存在しないが、Vimでは仮想的にそういうキーがセットされており、好きなキーを`leader`キーとして使うことができる。だいたいデフォルトでは`\`(バックスラッシュ)に当てられている。好きなキーと言っても大体のキーは`leader`以外のキーバインドで使っているので多くの場合は`Space`キーや`,`キーに割り当てられる。例として、`Space`キーに割り当てる。

```vimscript:init.vim
let mapleader = "\<Space>"
```
`leader`キーはmapleaderという変数に登録されていて、Vimではキーも文字列として管理されている。`""`で囲った文字列の中でバックスラッシュによってエスケープすることで、`<Space>`を文字列として扱うことができる。

#normalコマンド
`normal`コマンドはその名が表すように大変シンプルなコマンドである。`normal`コマンドは、引数に取った文字列のとおりにキーを押したことにするコマンドだ。コマンドにもなっていないような単純な操作を一括でしたいときにかゆいときに手が届く。

#ifコマンド
Vim scriptでは条件分岐もコマンドになっている。そう入っても書き方は割と普通である。

```vimscript
if 条件式
    "ここに必要な処理
elseif 条件式
    "ここに必要な処理
else
    "ここに必要な処理
endif
```
ここで注意したいのは、Vim scriptには真偽値型がないということである。だから`if`コマンドで評価した条件式は数値型に自動置換され、`0`以外なら真、`0`なら偽として進めることになっている。実際には条件演算子が結果に対応した数値を返すからあまり気にする必要はない。

#functionの定義とcall
長めの処理を関数として定義する。関数は基本的には以下のように定義する。

```vimscript
function! 関数名(引数1, 引数2)
    "ここに必要な処理
    return 戻り値
endfunction
```
`function`のあとの`!`はその関数が定義済みだった場合上書きしてこれにするという意味である。プラグイン等でも関数が定義されるので、知らない関数が存在していたときのためにつけるのが一般的なようだ。戻り地が必要なければ`return`コマンドを入れる必要はない。
また、**関数名は組み込み関数と区別するために大文字で始める必要がある。**

コマンドを自己定義することもできる。これも同じように**大文字で始める**。

関数は当然式の中に`関数名(引数1,引数2)`の形で呼び出して戻り値を使うことができるが、文として使いたいときは`call`コマンドを使う。

```vimscript:init.vim
function! Myterm()
    split
    wincmd j
    resize 10
    terminal
    wincmd k
endfunction

command! Myterm call Myterm()

if has('vim_starting')
    call Myterm()
endif
```
これは、画面を上下に分割して下側に高さ10のターミナルを開くための関数`Myterm()`を定義した例である。

また、`command!`で始まる行によってこの関数をコマンドとしても呼び出せるようにしている。

さらに、その下では、`if`コマンドを使ってVimの起動時に`Myterm()`を自動実行するための文である。`init.vim`は起動時に自動で読み込まれ、その間`vim_starting`というステータスが有効になる。これを踏まえて、`if`コマンドと`has()`関数を使って起動時の読み込みでのみターミナルを開く用にしているというわけだ。

#autocmd
`init.vim`に書いたスクリプトは読み込まれたときに即座に実行されるが、`autocmd`を使うことで特定のタイミングに自動実行することができる。

書き方は`autocmd [イベント] [イベントの引数] [コマンド]`といった感じである。イベントはたくさん存在するが、[ドキュメント](https://vim-jp.org/vimdoc-en/autocmd.html#autocommand-events)を参照して自分で確認してほしい。

以下は、`filetype`オプションが`javascript`のとき、インデントの幅を特別に2にする設定である。

```vimscript:init.vim
autocmd FileType javascript set shiftwidth=4 softtabstop=4
```
`FileType`イベントは、`filetype`オプションが設定されたときに発生するイベントで、それが`javascript`に等しいときは`set shiftwidth=4 softtabstop=4`を実行する。

#筆者の実際のinit.vim
最後に、私の`init.vim`の一部を公開しておく。どんな設定をすればいいかわからない人は参考にしてほしい。

```vimscript:init.vim
"行番号を表示
set number
"タブ文字の代わりにスペースを使う
set expandtab
"プログラミング言語に合わせて適切にインデントを自動挿入
set smartindent
"各コマンドやsmartindentで挿入する空白の量
set shiftwidth=4
"Tabキーで挿入するスペースの数
set softtabstop=4
"カレントディレクトリを自動で移動
set autochdir
"バッファ内で扱う文字コード
set encoding=utf-8
"書き込む文字コード : この場合encodingと同じなので省略可
set fileencoding=utf-8
"読み込む文字コード : この場合UTF-8を試し、だめならShift_JIS
set fileencodings=utf-8,cp932
"Vimの無名レジスタとシステムのクリップボードを連携 : ダメならxclipをインストールで使えるかも
set clipboard+=unnamed,unnamedplus
"eコマンド等でTabキーを押すとパスを保管する : この場合まず最長一致文字列まで補完し、2回目以降は一つづつ試す
set wildmode=longest,full

"LeaderキーをSpaceに設定(これだけでは意味をなさない)
let mapleader = "\<Space>"

"C++,Java等のインラインブロックを中括弧付きのブロックに展開
nnoremap <C-j> ^/(<CR>%a{<CR><Esc>o}<Esc>
"カーソル上の単語を置換
nnoremap <expr> S* ':%s/\<' . expand('<cword>') . '\>/'

"挿入モード終了時にIMEをオフ
inoremap <silent> <Esc> <Esc>:call system('fcitx-remote -c')<CR>

"下部分にターミナルウィンドウを作る
function! Myterm()
    split
    wincmd j
    resize 10
    terminal
    wincmd k
endfunction
command! Myterm call Myterm()

"起動時にターミナルウィンドウを設置
if has('vim_starting')
    Myterm
endif

"上のエディタウィンドウと下のターミナルウィンドウ(ターミナル挿入モード)を行き来
tnoremap <C-t> <C-\><C-n><C-w>k
nnoremap <C-t> <C-w>ji
"ターミナル挿入モードからターミナルモードへ以降
tnoremap <Esc> <C-\><C-n>

"ファイルタイプごとにコンパイル/実行コマンドを定義
function! Setup()
    "フルパスから拡張子を除いたもの
    let l:no_ext_path = printf("%s/%s", expand("%:h"), expand("%:r"))
    "各言語の実行コマンド
    let g:compile_command_dict = {
                \'c': printf('gcc -std=gnu11 -O2 -lm -o %s.out %s && %s/%s.out', expand("%:r"), expand("%:p"), expand("%:h"), expand("%:r")),
                \'cpp': printf('g++ -std=gnu++17 -O2 -o %s.out %s && %s/%s.out', expand("%:r"), expand("%:p"), expand("%:h"), expand("%:r")),
                \'java': printf('javac %s && java %s', expand("%:p"), expand("%:r")),
                \'cs': printf('mcs -r:System.Numerics -langversion:latest %s && mono %s/%s.exe', expand("%:p"), expand("%:h"), expand("%:r")),
                \'python': printf('python3 %s', expand("%:p")),
                \'ruby': printf('ruby %s', expand("%:p")),
                \'javascript': printf('node %s', expand("%:p")),
                \'sh': printf('chmod u+x %s && %s', expand("%:p"), expand("%:p"))
                \}
    "実行コマンド辞書に入ってたら実行キーバインドを設定
    if match(keys(g:compile_command_dict), &filetype) >= 0
        "下ウィンドウがターミナルであることを前提としている
        nnoremap <expr> <F5> '<C-w>ji<C-u>' . g:compile_command_dict[&filetype] . '<CR>'
    endif
endfunction
command! Setup call Setup()

"ファイルを開き直したときに実行コマンドを再設定
autocmd BufNewFile,BufRead * Setup
"RubyとJSではインデントを2マスにする
autocmd FileType ruby,javascript set shiftwidth=2 softtabstop=2
```
これが、私が実際に使っている`init.vim`のうち、プラグインや自作ツールが絡まない部分である。私はまだこれで完全に満足しているわけではないので、これからもっと快適なVimを作り上げていきたいと思っている。
`init.vim`は本当に個性の出るファイルなのでみなさんもぜひ最高の設定を目指して作ってみてほしい。

#最後に
**わからなかったらヘルプ機能を使おう。**`:h [知りたい単語]`でだいたい出てくる。

#参考
[Vim日本語ドキュメント](https://vim-jp.org/vimdoc-ja/)
[Vimスクリプト基礎文法最速マスター - 永遠に未完成](https://thinca.hatenablog.com/entry/20100201/1265009821)
[vimrc基礎文法最速マスター - 永遠に未完成](https://thinca.hatenablog.com/entry/20100205/1265307642)