🧘‍♂️

Vimの思想について

2023/10/29に公開

はじめに

大きなタイトルにしてしまいましたが、自分がVimを使ってきて思った、その背後にある哲学を、Vimの「Zen」としてをまとめました。

The Zen of Vimというタイトルで、こちらにシンプルなバージョンをアップしていますが、少し展開して書いてみたいと思います。

Vimの言語を話す

  1. Speak Vim, not memorize.
  • Vimは、構成的(composable)な言語を使っている
  • テキストの構造をテキストオブジェクトとして理解している

Vimの多種多様なコマンドを駆使して素早くナビゲートしたり変更したりするのが、Vimmerにとって一番気持ちいいところかもしれません。これによって「効率的に」仕事ができるし、格好づけにも最適です。その根底にあるのは、Vimの多くのコマンドは、英語のプレーズの略語となっています。この表現のシンタックスが宣言的なもので、ほとんど我々が喋る言葉のままです。

よくあるパターンというのは、動詞(operator)+名詞(text objects)となっています。たとえば、

  • dw as delete word,
  • cl as change letter,
  • yib as yank(copy) in block,
  • va[ as visual around [ bracket
  • > 2j as shift 2 lines below

このパターンは理解しやすく、拡張もしやすい。実際に、多くのプラグインもこのパターンに沿って実装されています。例えばvim-surround, vim-visual-multi, Commentなど。

このパターンを理解するために、まずOperator(演算子)、とテキストオブジェクトは避けられません。

Operator

よく使われる演算子を羅列します。下記のリストのフルバージョンは、:help operatorで確認できます。

Operator Description
c change
d delete
y yank into register (does not change the text)
~ swap case (only if 'tildeop' is set)
g~ swap case
gu make lowercase
gU make uppercase
> shift right
< shift left
zf define a fold

ここでいくつかのルールとして

  • motion-count-multiplied 例えば10jで十行下へ移動、d2kで上2行を削除など
  • operator-doubled 例えばyy, dd, cc, >>とか、2回繰り返すとカーサーのある行に動作する
  • upper case operator 例えばc->C、d->D, y->Yのように、現在のカーサーから行の最後まで動作するケースと、対になっている(逆の機能を持つ)ケース、例えばp->P, o->O, n->N, a->A, i->I, f->Fなど)に大まかに分けられる
  • exclusive inclusive 例えば、yiw vs yaw, cib vs cabの違いで、テキストオブジェクトの内部(inner)だけか、境界線も含めるか(around)

があります。これらのルールは他のプラグインにもよくみられるので、Vim wayに慣れればプラグインもすぐに使い慣れるのがここにあります。

演算子は主に、テキストの編集に使われますが、編集先を素早く見つけるために、移動(motion)の方法も非常に豊富です。例えば、

  • gg/G as top and end of file
  • ^/$ as begin/end of line
  • w as word
  • e as end(of a word)
  • b as back
  • 2k as up 2 lines
  • f,F or t, T to locate the character
  • ?, / to search pattern up and down

移動→編集という流れで、両者を結合して考えると、

  • cf' as change until find '
  • d/<pattern> as delete until a pattern

といった使い方ができるのです。

テキストオブジェクト

この概念は非常に大事で、Vimを接し始めた頃に知っておけばよかった概念のトップ1です。

Vimの視点から見て、テキストはword, sentence, paragraph, blockといった基本的なユニットから構成されています。これも人間にとって、テキストをうまく整理するために違う粒度の単位があるとかなり好都合なものです。

  • word スペース、-で区切られる
  • sentence ., !, ?, スペースまたはタブで区切られる
  • paragraph 空白の行で区切られる
  • block (), [], {}, <>といった各種括弧
  • quotations '', "", \``といった各種クォーテーション
  • tag <h1></h1>といったHTML/XMLのタグ

上記のVimデフォルトのテキストオブジェクトだけでなく、カスタムテキストオブジェクトを作ることができるので、さらに可能性が広がります。

neovim0.9から、treesitterとの言語解析のプラグインをマージしましたが、treesitterの力を借りて言語別のテキストオブジェクトが使えるようになります(プラグイン)。例えば、定義された関数を全部削除したい時に、dafでaround functionで、関数のテキストオブジェクトを単位に編集可能になります。

DRY原則

  1. DRY

Don't type it twice

全ての編集は繰り返し可能。ここで主に2つの考え方があります。

ドットコマンド

.を押すことで、最後に行った編集操作を繰り返すことができます。例えば、apple and bananaに対して、diw -> e -> .を行うと、diwの操作を2回以上押さなくて良いのです。また、インデントを治す時に、愚直になんかも>を押すよりも、shift+. -> ....で良いでしょう(もちろん、フォーマッターを使うのが最もですが)。

マクロ

ドットは最後に行ったコマンドしか記録しないのですが、より複雑な操作を行う場合、マクロを使うのが良いでしょう。マクロはVimのレジスター(register、詳細は次節)に任意のキーストロークの連続をレコーディングしてくれます。qを押すとレジスターを選ぶようになり、次にa-zとかのキャラクターを入れると、該当レジスターにコマンドを記録することができます。もう一度qを押すとレコーディング終了です。

複雑な操作をレコードする最中でよく間違いをしてしまいます。一つのtipsとして、コマンド自体をまずテキストとして記録し、それが思う通りの操作になるか検証をしてみます。次に使いたいレジスターに、テキストをコピペすれば、次回は@qとかで呼び出すことができるのです。なお、マクロ版のドットコマンドとして、@@では最後に実行されたマクロを実行してくれるのです。

時間旅行者となれ

  1. Be a time traveller.

全ての編集は繰り返し可能だけではなく、取り消しもできるのです。実際に使うのは、主にuでundoをし、逆にundoをやめたい場合はctrl+rでredoを行います。

時間旅行ができるのは、それなりの理由があります。Vimは、そのための情報を色々と覚えてくれています。

レジスター

Vimには10種類のレジスターがあります。:registersで現在のレジスターの中身を確認できます。呼び出すには"<register>を使います。なので、次のリストは全部"から始まります。

  1. The unnamed register "" こちらはd,c,s,x,yといったオペレータの対象テキストが生まれます。クリップボードには近いものです。VimのクリップボードをOSと共通するためによくこれを利用します。
  2. 10 numbered registers "0 to "9 この辺りは、y(copy)とdの中身が保存されます。0はコピー、1-9は削除です。
  3. The small delete register "- 一行より少ない削除のテキストを保存
  4. 26 named registers "a to "z or "A to "Z 実際にレジスターに何かを記録させる(例えばマクロ)時に基本こちらのレジスターを利用します。
  5. Three read-only registers ":, "., "% ドットは先ほどのドットコマンドです。%は現在のファイル名、それで:は最後に実行されたコマンドラインのコマンド(編集ではなく、:でコマンドモードに入った時のコマンド、例えば:help registers)。
  6. Alternate buffer register "# こちらはほぼ使うことがなく、ファイルの間を切り替えする際に、現在のファイルの一個前に開いたファイルのパスが保存されているようです。
  7. The expression register "= こちらはテキストを保存するよりも、入力された内容(expression)を評価してその実行結果をペーストすることです。例えば、Insertモードでctrl+=を押して、次にsystem('ls')を実行すると、その結果が挿入されます。
  8. The selection registers "* and "+ unnamedレジスターをクリップボードとして使うと、Vimでコピーされた(yに限らず、"に保存された)テキストはOSのクリップボードにも現れます。逆のパターンとして、他のプログラム、例えばブラウザーで何かをコピーした時、こちらのレジスターにほぞんされるのです。OS->Vimもクリップボード("レジスター)を使いたい場合、unnamedplusに設定する必要があるのです。
  9. The black hole register "_ 何も保存されないレジスター。削除されたテキストは通常1,2,3に保存されますが、痕跡を残さない方法はこれを使うのです(実際使ったこと全くないが)。
  10. Last search pattern register "/ パターン検索する時のパターンを保存してくれます。

マーク

レジスターは様々な情報を保持してくれます。それに対して、マークは言葉通り、bookmark的にカーサーの場所を記録してくれるのです。:marksで現在のマークを確認できます。呼び出すには'<mark>もしくは`<mark>を使います。区別というと、'の場合では、カーサーの位置した行の一個目の文字にマークをつけるが、`の場合では、行だけではなく列まで精確にマークする。

  • 'a - 'z 一つのファイル内で有効
  • 'A - 'Z ファイルの間にも有効
  • '0 - '9 shada(shared data)ファイルにのみ有効かつ手動セット不可

移動と編集の履歴

これらの履歴は、先ほどのレジスターである程度情報を取れるものの、レジスターの目的は別にあります。移動と編集の履歴はスタックの形で、jumplistとchangelistに保存されています。

jumplistはカーサーのいた場所を覚えてくれています。ctrl+i/oで新しい・古い順でジャンプできます。最後の居場所(stack.peekのような)と現在のカーサーを``で素早く行き来できます(''は同様、マークと同じ区別)。

changelistは、jumplistと結構近く、要するに変更を行った場所を記録してくれるのです。g,g;で新しい・古い順でジャンプできます。こちらは:changesで確認できます。

ファイル間の移動と編集

まず検索手段として、デフォルトのvimgrepがありますが(実際はfzf, rg, telescopeを使うのがおすすめ)、ここで主に話しておきたいのはqflist(quick fix list)です。

qflistの本来の目的は、名前通り、ファイル間のエラー情報を集めて、リストアップしてすぐにエラーの場所に辿り着いてフィックスするためのものです(いわゆるedit/compile/fixサイクル)。ただ、Vimではもちろんエラーだけではなく(trouble.nvimというプラグインが強力)、何かを検索してそのマッチする結果をリストとしてまとめるより汎用的な機能となっています。

また、qflist自体も、スタックとして保存されているので、新旧のqflistの間に行き来することが可能です(cnewer, colder)。

qflistにリストされているエントリーに対して、全てのエントリー所在のバッファーに対してコマンドを実行することが可能です。

  • cdo {cmd} 全てのエントリーに対してコマンド実行。例えばよく使うグローバル置き換え(cdo %s/<target>/<new>)とか。
  • cfdo {cmd} 上記と近いのですが、fはファイルという意味で、エントリーより粒度が大きくなります。

このように、本来のアイディアとなるedit/compile/fixというサイクルよりも、qflistは、search/index/motion&editとのサイクルになるのです。

モードを忘れよう

  1. Feel no modes.

The Ideal: One Keystroke to Move, One Keystroke to Execute.

何が自然なのか

かなり主観的な部分かもしれません。VimはよくModal Editorの代表として挙げられ、normal, insert, visual, terminalといったモードの切り替えと、それぞれのモードにおける違う動作パターンを持つことが強調されています。

もちろん、ノーマルモードのおかげで、移動と編集が非常にやりやすくなりました。ただ、モードの切り替えは、Vimの特徴的な部分としてあまり見過ぎない方が良いのではないかと考えています。

というのは、開発者にとってほとんどの時間はコードを書いているよりも、何かを読んでいるのです。Robert Martin氏が名作Clean Codeで言われているように、1:10の比例になっているらしい。この長い読みの流れの中で、「たまたま」何か変更を行います。ノーマルモードは、この「読みモード」と称するべきで、むしろ開発者の動きにとって自然なもので、逆にいつまでもinsertとの「書きモード」にしか存在できない一般的なテキストエディターのほうが不自然です。

このコーディングの流れを、移動->編集(undo,redo含め)->移動とのサイクルに集約できます。さらにinsertモードに入らなくても、1で示したように、normalモードで編集できます。normalモードにおいての、効率の良い各種の検索と移動手段が、編集というたまたま発生する行為とよりスムーズに連携できるようになっているのです。insertモードは、どちらかと言うと、normalモードで完成できない動きを補完しているとも言えます。

The Smell

visualモードについて、Chris Toomeyさんが彼のトークの中で、the smellと主張していました。つまり、visualモードに入っているのであれば、よりVimらしい解決方があるのでは?、との匂いです。

この観点について半分賛成です。Visualモードは名前通り、見ているものからセレクトしているので、1で言及しているテキストオブジェクトを利用することが難しくなります。むしろ、2つ全く違う視点、客観的と主観的、表面的と意味的となるのです。

具体的に言うと、例えば、yでコピーした内容を、カーサーの単語に入れ替えたい場合、自分はよくviw(visual select in word)して、pをやっていました。ただ、この動きは非常にthe Vim wayではなく、繰り返しができないのです。理想的な動きというのは、yiw -> siw(substitute in word)となり、次の箇所で.でリピートするのです(siwはプラグイン必要)。

ただ、反対する半分というのは、Visualモードというか、マルチカーサーを使う時があるのです。これは、意味(テキストオブジェクト・パターン)に対して関心を持たない時の選択肢です。例えば、どの単語か関係なく、とりあえず次の10行に括弧をつけて、最後にコンマをつけたいといった、繰り返しの動きを、マルチカーサーを介して1回で終わらせる場面です。無論、ほとんどの場合はパターンマッチや、ドットやマクロで対処できるのだが、そこの考え方と好き嫌いはあるので、自分はマルチカーサーという「Vimらしくない」やり方に肩を入れたいのです。

いずれにしても、Vimの誇らしさ、強み、特徴というのは、モードにあるわけではないと考えています。モードの切り替えは、Vimの考え方を発揮させるための土台にすぎません。Vimの考え方に慣れると、モードも自然に忘れるだろう。

自分だけのVim

  1. Embrace the world, make it your own.

Vimは非常にconfigurable & extensibleなエディターです。Vimのマインドセット、考え方を学び、其の後はいかに自分が気持ちよく、効率よく使えるか、設定をいじりまくればよいです。

ただ、これを始めると結構挫折しやすくなるので、以前こちらでもいったように、AstroNvim, LunaVim, LazyVim, NvChadといったpreconfigされたものをスタート地点とするのがおすすめです。

また、プラグインの更新などをあまり頻繁にやらない方が良いのです。こちらの記事のように、いつの間にかプラグイン無限地獄に陥る可能性があり、結局時間を多く使ってしまい、諦めることもあり得ます。自分は現在、NvChadを使っているのですが、割と安定しているので、基本月単位で設定を更新しているのです。

最終的に、PDE a.k.a Personal Development Environmentを構築し続けるのが、Vimmerのロマンだと考えています。

終わりに

自分が開発を学び始めた頃、初めてVimを触った時、案の定ファイルから出られない問題で頭を抱えました。当時はVimとは何かすらわからず、シェルのデフォルトエディターとして無理やり使わせられたのです。

その後、知り合いの勧めでVimというハードモードを選んでみました。初心者フレンドリーなVS CodeのVim pluginを使ったり、Vimをセットアップして何回も失敗してVSCodeに戻ったり、最終的にVim/NVimのDistroを使って、SpaceVim,AstroNvimを経て、NvChadに辿り着きました[1]

なぜVimを使い続けてくるのか、今振り替えてみると2つあるかと思います。

  • Vimは移動と編集の操作を簡潔かつ効率よく言葉で伝えるのです。これは自分の考えの延長として、認知的負荷がないほど体に覚えられています。
  • Vimを学ぶ道には終わりがないところ。常に前に進められる感覚が、何よりです。

おすすめの読み物

この記事を作成する際には色々と記事と本を参考にしました。その中で特におすすめしたいものをリストアップしました。

脚注
  1. 2024/01/17 追記 こちらの記事を読んで、かなり共感できる気がしました。vimの学ぶステージをignorance->curiosity->denial->embrace->enlightmentとまとめられています。自分も学ぶ経験とも結構被るところが多かったです。 ↩︎

Discussion