明日から使える Command-line window テクニック

この記事は [Vim Advent Calendar](https://qiita.com/advent-calendar/2019/vim) 7 日目の記事です．6 日目の記事は daisuzu さんによる [VimのtagfuncでLSPを使う](https://daisuzu.hatenablog.com/entry/2019/12/06/005543) でした．8 日目は gorilla0513 さんの記事が入る予定です．

# はじめに

皆さんは Vim の **Command-line window** をご存知でしょうか．
名前に馴染みがなくとも，あるいは「意図して」使ったことがなくとも，見たことのある方なら多いかもしれません．そう，Vim を終了しようと思って `:q` と押したつもりが間違って `q:` と押してしまったときに起動してしまう，あの画面です．

![vim1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/441268e3-5d5d-325e-386e-f1a68f5213d1.png)

Vim 初心者にとっては初見殺しのこの画面．私も初めて出会ったときはとても困惑しました．Vim を終了させようとしたのに，終了しないどころか代わりに変な画面が出てきた．得体の知れないこの画面をどう閉じれば良いかも分からない（`<ESC>` を押しても消えません．ウィンドウだと分かれば `:q` で閉じると分かるんですが）．そんな最悪の出会いが気づかぬうちにトラウマになっていたのかもしれません．私は Vim を始めてから1年の間ずっと，このウィンドウの存在を知りつつも，なんとなく使用を避け続けてきました．しかしつい1ヶ月ほど前にこの機能の便利さを知り，今は最大限活用しようと一生懸命リハビリ（？）に励んでいます．
この記事は，私と同じようにトラウマに陥り避けてしまっていた（かもしれない）Vimmer に捧げる Command-line window の布教記事です．


## 対象読者

* Vim の Command-line window ってなんぞや？という人
* Command-line window ねー，私あまり好きじゃないなあー，という人
* Command-line window ねー，あまり使いこなせてないなー，という人

私はピュアホワイトな Vimmer では全くありませんが，今日お話する機能にプラグインの話はほぼ出ません．したがって，素の Vim でなんとかしたいホワイトな Vimmer にもおすすめです（ただし，後半で `~/.vimrc` への設定追加は行います）．
なお本記事はある程度最近の Vim なら共通する話題だと思っていますが，私は Neovim を普段使いしているため，Neovim 特有の話が混入しているかもしれません．不正確な箇所などが発見された場合は，是非コメントなどでご指摘いただければ幸いです．

# Command-line window の基本機能

**Command-line window** とは， Vim/Neovim に搭載されている機能の一つです．デフォルトでは，Normal モードで `q:` と打つことで出現します．

（再掲）
![vim1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/441268e3-5d5d-325e-386e-f1a68f5213d1.png)

かつての自分が間違えてしまったように，このキーマップは `:q` という Vim を終了するためのコマンドと極めて紛らわしいため注意してください．気になる方はキーマップを変更するのもありだと思います．なお，間違えて開いてしまった場合は落ち着いて `:q<CR>` と押下しましょう（ `<CR>` はエンターキー）．Command-line window は特殊なウィンドウであり，通常のウィンドウと同様に `:q` で閉じることができます．

さて，画面に並んでいるのは，自分が過去に打ったコマンドです（上にいくほど古い）．デフォルトでは自分が打った直近6種類のコマンド + 空行が表示されるはずです[^1]．履歴は j/k でスクロールでき，エンター (`<CR>`) で実行することができます．たとえば先程の画面から， `:h history` （`:help history` の略で， `history` についてのヘルプを開くためのコマンド）を再度実行したくなったとします．Command-line window を開いたときにウィンドウの最下端にカーソルが合ったとすると，そこから`k` を2回押せば `h history` と書かれた行の上にカーソルが来ます．後はそこで `<CR>` を押せば以下のようにヘルプ画面が表示されます．Normal モードから `:h history<CR>` とコマンドを打ったときと同じ効果が得られるというわけです．

[^1]: 一度に表示されるコマンドの数（＝ウィンドウの行数）は `'cmdwinheight'` オプションで指定可能です．

![スクリーンショット 2019-11-26 23.07.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/01bb2251-7185-bf5d-649b-f916e4d7cef1.png)

これが Command-line window の基本的な使い方となります．

なお，ノーマルモードで `q/` と打つと検索履歴が表示されます．`q:` とは違い，「暴発」してしまうことがほとんどないため，`q:` と比べると知名度が低いかもしれません．昔の検索を使いまわして再検索をかけることができます．

![スクリーンショット 2019-12-03 21.23.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/bad30cd8-96f4-817c-a288-9a11a5d23d17.png)

# Command-line window のすごいところ

今の話だけ聞くと，Command-line window は過去の履歴を参照して使い回すためだけのものだ，と思われるかも知れません．1ヶ月前の私も同じように思ってました．通常のコマンドラインだって補完はあるし履歴は辿れるし十分だ，わざわざウィンドウで開く必要なんかない．

全然違いました．Command-line window にはすごい所がたくさんあったのです．

## その1: 通常のバッファと（ほぼ）同じように内容を編集できる

Normal モード・Insert モードを駆使する Vimmer にとって，Command-line モード [^2] でのテキスト編集は，通常のウィンドウにおける編集ほど自由度が高いとはいえません[^3]．Normal モードほど自由にはモーションやテキストオブジェクトを使えませんし，有効なモーション（行頭・行末移動など）も使うには Command-line モード特有のキーマップを覚える必要があります．通常のウィンドウでは `hjkl` を使いこなしているのに，Command-line モードではついつい矢印キーを使ってしまっていませんか？私のことです．通常のウィンドウだとテキストオブジェクトでサクっと消せる文字列も，Command-line モードでは Deleteキーやなどでちまちま消してしまっていませんか？私のことです，ごめんなさい．

[^2]: Normal モードから `:` キーを押すと下一行にコマンドを書けますよね．あれを打っているときのモードです．今回説明する Command-line window とは違います．
[^3]: 勿論，Command-line モードにも便利なキーマップはたくさんありますが...

その問題を綺麗に解決してくれるのが Command-line window です．Command-line window は編集可能なウィンドウなので，通常のウィンドウと同じキーマップを使ってコマンドを編集することができます．これは長いファイルパスの途中をいじりたいときや，`:s` で置換するときに複雑な正規表現をあれこれ編集したいときにとても便利です．

たとえばこんな複雑な正規表現が相手でも...
（再掲）
![スクリーンショット 2019-12-03 21.23.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/bad30cd8-96f4-817c-a288-9a11a5d23d17.png)
`f[` と打つだけで次に出現する `[` の位置まで移動できますし...
![スクリーンショット 2019-12-03 21.25.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/f06fcb76-bcfd-46db-a381-680a885b7766.png)
`ci[` と打つだけで今移動してきた角括弧 `[0-9a-z\<]` の中身を変更することができます．
![スクリーンショット 2019-12-03 21.27.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/1f22d03d-64d0-da16-f72f-5673c7e9136b.png)
ノーマルモードと変わらない操作性を実現できるということです．便利ですね！

ただし，一部のキーマッピングについては通常のウィンドウでの機能から変更があったり，使えなかったりする点には注意が必要です．例えば Command-line window ではエンターを押すとそのコマンドが実行されます．これは Insert モードでも例外ではありません．`<C-c>` も「現在いる行を Command-line に移して Command-line モードに遷移する」機能にマッピングされています．また，Command-line window を開いている間は他のウィンドウには移れません．他のバッファに遷移して文字列を自由にヤンクしてまた Command-line window に戻る... というわけにいかないのは少し残念ですね．

なお， Command-line window での編集は一時的なものであり，履歴そのものには反映されません．つまり Command-line window で過去のコマンドを変更/削除しても，ウィンドウを閉じてまた `q:` で開き直せば元通りです．したがって過去の改変はできませんが，逆に言えば気兼ねなく好き勝手編集できる，ということでもあります．なお，「編集してから `<CR>` を押すなどして実行したコマンド」については新たにコマンドを打ったものとして履歴に保存されますから，後々編集することができます．

## その2: 検索や置換もできる

Command-line window では，通常のウィンドウと同様に `/` による検索や `:` から始まるコマンドによるジャンプ・編集ができます．これも実用上かなりありがたい機能です．例えば履歴から任意の文字列を検索することができますし，`:s/hoge/fuga/g` で，あるコマンドの全ての hoge を fuga に置換することもできます．`:g` や `:v` を使った絞り込みも可能です（活用例は後述）． `q/` で開いた検索画面を「検索」することだって可能です（実用性があるかは分かりませんが）．

## その3: コマンドラインからも開ける

Command-line モードでいくつかコマンドを打った後に，「あ，やっぱ Command-line window で編集したい」と思うことがあるかもしれません．それ，大丈夫です．コマンドライン上で `<C-f>` を押すだけです．

![スクリーンショット 2019-12-03 21.49.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/dbf2d6c0-a58c-e573-e8a2-4190ab354754.png)
このように，途中までコマンドを打ってしまった状態でも... `<C-f>` と押せば
![スクリーンショット 2019-12-03 21.50.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231599/c20fa447-2672-6363-6bef-d50aed3716aa.png)
ほらこの通り．簡単に Command-line window に遷移できます．

`:` から始まるコマンドラインに限らず，`/` を使った検索の途中で `<C-f>` と打つと検索履歴がたどれます．さらにマイナーですが，expression レジスタの中でも使えます．Insert モード中で `<C-r>=` と打った後，`<C-f>` を押してみてください．expression レジスタを使ったことがある人は，その履歴が表示されるはずです．

（追記）
[Vim Short Tips Advent Calendar 2019](https://qiita.com/advent-calendar/2019/vim-short-tips) の7日目のツイート：

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr"><a href="https://twitter.com/igrep?ref_src=twsrc%5Etfw">@igrep</a> さんに教えてもらったネタ<br>: を押して Ex コマンドを入力中に &lt;C-f&gt; を押すのです<br>コマンドラインウィンドウが立ち上がり、入力中のコマンドをいつもの感覚で編集出来ます<br>正規表現を微妙に間違えて修正するときとかに便利<br>// これ知るまでカーソルキーで頑張って修正してた<a href="https://twitter.com/hashtag/vimtips_ac?src=hash&amp;ref_src=twsrc%5Etfw">#vimtips_ac</a></p>&mdash; てんなし (@tnnsh1) <a href="https://twitter.com/tnnsh1/status/1203151098606841856?ref_src=twsrc%5Etfw">December 7, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

あっ... 被ってしまった... てんなしさん，すいません...
逆に言えば，同日に2人が広めたくなるぐらい素晴らしい機能，ということですね！
[Vim Short Tips Advent Calendar 2019](https://qiita.com/advent-calendar/2019/vim-short-tips) のほうもどんどん盛り上げていただけると嬉しいです〜

# Command-line window を使いこなしてみる

さて，すごいところをたくさん知ったので，今度は Command-line window をより使いやすくするためのカスタマイズを行いたいと思います．以下は私が試みたカスタマイズ例です．

## 使わないバーを消す

通常の window で `set number` を設定している人は，Command-line window でも行数が表示されます．全く無意味とは言えないものの，1000を超えてくるとちょっと邪魔です．また，`set signcolumn=yes` の設定になっている人は，Command-line window にも signcolumn （エラーなどを表示するために左端に表示される列）が表示されてしまいます．number （行数表示）はともかく，signcolumn の方はまず使わないと思います．`.vimrc` に以下のような設定を書いて，さらっと無効化しましょう．

```vimscript
" 行数を非表示
autocmd CmdwinEnter [:/?=] setlocal nonumber
" signcolumn を非表示
autocmd CmdwinEnter [:/?=] setlocal signcolumn=no
```

Command-line window に入ったときに特定のコマンドを走らせたい/特定のオプションを有効にしたい，というニーズのために，`CmdwinEnter` というイベントが用意されています．詳しくはヘルプ参照 (`:help CmdwinEnter`)．

（2019/12/08 追記）上のコマンドでは，検索履歴のウィンドウで動作しない可能性があります．詳しくは以下の追記本体を展開してご覧ください．

<details><summary>（2019/12/08 追記本体）</summary><div>
CmdwinEnter の後の `[:/?=]` というのは，以下の4種類の Command-line window を対象とする，という意味です（詳しくは `:help cmdwin-char`）．

- `q:` や `:<C-f>` で開かれるウィンドウ（Ex コマンドの実行履歴）
- `q/` や `/<C-f>` で開かれるウィンドウ（前方検索履歴）
- `q?` や `?<C-f>` で開かれるウィンドウ（後方検索履歴）
- 挿入モードで`<C-r>=<C-f>` としたときに開かれるウィンドウ

しかし， @tsuyoshi_cho さんから「検索履歴のウィンドウを指定する `/` にはエスケープが必要ではないか」とのご指摘をコメントでいただきました．Neovim では再現できませんでしたが，Vim8 の環境では `/` にエスケープをして `\/` とする必要がある可能性があります．また， Neovim 環境下でも `?` にはエスケープが必要でした（確認しておらず申し訳ありませんでした）．現時点での私の結論としては，複数環境下で動作するような `.vimrc` を書く場合

```vimscript
" 行数を非表示
autocmd CmdwinEnter [:\/\?=] setlocal nonumber
" signcolumn を非表示
autocmd CmdwinEnter [:\/\?=] setlocal signcolumn=no
```

とエスケープするのが最も無難ではないかと思います．[vim-jp の issue](https://github.com/vim-jp/issues/issues/1319) にも tsuyoshi_cho さんが報告を行ってくださってますので，今後の動向はそちらをご覧ください．

</div></details>


## 自明なコマンドを履歴から消す

`:q`, `:q!`, `:wq` といったコマンドは，単純すぎてコマンド履歴からたどりたくなるようなものではありません．しかし，そういったコマンドはかなり頻繁に打たれるため，履歴の一番前の方[^5]に出てきて本質的なコマンドを探す際の邪魔になります．かわいそうですが，CmdwinEnter の時点で表示から消してしまいましょう．特定の正規表現に合致する行を消すことのできる `:g/(regexp)/d` コマンドを使います．

[^5]: 行で言うと最終行のほうですね，ややこしいですが．すぐ見える位置に出てきてしまうということです．

```vimscript
autocmd CmdwinEnter : g/^qa\?!\?$/d
autocmd CmdwinEnter : g/^wq\?a\?!\?$/d
```

## 今までに引いたヘルプ一覧へのショートカット

ヘルプや置換などよく使うコマンドは，
それらを絞り込むコマンドを予めキーマップなどに割り当てておくと時間短縮につながるかもしれません．
たとえば以下のようにすれば，ノーマルモードで `<Space>?` と打つことで今までに引いたヘルプの一覧を取得できます．

```vimscript
nnoremap <silent> <Space>? q::v/\v^h%[elp] /d<CR>:nohlsearch<CR>
```

また以下のようにすれば，ノーマルモードで `<Space>/` と打つことで今までに行った置換操作の一覧を取得できます．

```vimscript
nnoremap <silent> <Space>/ q::v/\v^(\%<Bar>('[0-9a-z\<]<Bar>\d+),('[0-9a-z\>]<Bar>\d+))?s\//d<CR>:nohlsearch<CR>
```

ただし，range 部分の正規表現がわりとテキトーで，自分が実用的に使う「1行置換」「全置換」「行数，マーク，VISUAL モードによる範囲指定置換」しか対応していません．本当は `$` や `.` を用いた range にも対応すべきなのですが，面倒なので放置しています．もっと幅広い range に対応させたい方は，いいかんじに改善していただければと思います！

# 終わりに

最後まで読んでくださりありがとうございました．Command-line window は使うタイミングを見極めればかなり強力で，複雑なコマンド操作を書きたいときの強い味方だと思っています．本記事の布教によって Command-line window を使う人が少しでも増えたら嬉しい限りです．
本記事を読んでくださった方々は，是非他の [Vim Advent Calendar 2019](https://qiita.com/advent-calendar/2019/vim) や [Vim2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/vim2) の記事も読んでみてください．きっとさらに Vim の魅力を知ることができると思います！