## はじめに
### VimとEmacs
プログラミングを始めてからずっとVimというエディタを使っていましたが、今年の2月からEmacsを使うようになりました。
プログラマには妙なこだわりを持っている人が多いです。とくに使用エディタは論争の種になり、この2つのエディタはよく比較して語られます。
どちらの世界も体験した僕にとってこの2つのエディタは次元の異なるもののように見えます。Vimはエディタとしての機能と効率を突き詰めたものであり、一方でEmacsは拡張性豊かなLispでさまざな機能を取り込んでいこうとします。どちらが優れているというわけではありません。何しろ次元が違っているのです。二次元が三次元より劣っている、なんてことはないように、どちらを選ぶも好き好きでしょう。
しかし、この不毛な議論を価値観の違いという一般論で片付けてしまうのは少し勿体無い気がします。そこでVimmerが言おうとしていることに積極的に耳を傾け、この議論を冷静に深めてみたいと思います。
たとえばVimmerは「Emacsは愚鈍でキーバインドが許しがたいほど冗長だ」と言います。残念ながらこれは客観的にも正しい批判です。Vimは“モード”という特殊な概念を導入して多くのキー操作を単一キーでこなせるようにしています。Emacsの柔軟なカスタマイズ性がいくら素晴らしいと言えど、このキー操作の差は作業効率や指の疲れを考えると大きなトレードオフだと言わざるをえません。肝心のエディタとしての機能を削ってまで、その拡張性は必要なものなのでしょうか?
### エディタとしてのEmacs
Vimの素晴らしい点は、付属するコマンドが日常のあらゆる場面でのキーストロークを最小にするように考えられて設計されていることです。結果、それほどカスタマイズせずとも、標準のキーバインドに慣れさえすれば良いテキスト編集の習慣を身につけることができます。
一方で、Emacsはそれほど最適化されてはいません。日常でよく使うコマンドも多くは冗長なキーバインドになっています。そういう設計思想なのだ、と言われれば言い返しもしませんが、その思想が効率的なテキスト編集のために考えられたものでないことは明らかです。その点、Emacsはエディタとして良い設計とは言えないでしょう。
けれど、僕はEmacsが自在なカスタマイズの効くエディタだと聞きました。それならば、Emacsの上に自分に合ったエディタを再設計もできるのではないでしょうか。
僕はこのアイデアを実践し、純粋なエディタとしての機能でVimに勝負を挑むことにしました。
### エディタの再設計
今あるエディタを再設計するというのは奇妙なものです。自分が今まで慣れ親み、当たり前にしてきたことにすら疑問を投げかける心構えが必要です。そのために僕がしたのは、Emacsを疑うことでした。標準で割り当てられているコマンドは便宜上たまたまそこにバインドされているだけであって、何の意味もありません。それはちょうどお試しモードのようなものです。
僕はまず最も押しやすいホームポジションにはどのようなコマンドがあるべきかを考えました。最も指の移動距離の少ないこの重要なポジションを、ヘルプや行削除に譲っている理由はありません。僕はカーソルの移動をCtrl-bnpfからCtrl-hjklに変更しました(リスト1)。その影響でC-jはC-nに移動し、C-kはC-oに移動しました。C-oは使う場面がないので上書きしました。もし最初からC-oに割り当てられていなければ、僕はその機能を気にすることもなかったでしょう。そういうキーバインドは必要のないものです。
1 2 3 4 5 6 7 |
- リスト1 カーソル移動をCtrl-hjklに (global-set-key "\C-h" 'backward-char) (global-set-key "\C-j" 'next-line) (global-set-key "\C-k" 'previous-line) (global-set-key "\C-l" 'forward-char) (global-set-key "\C-n" 'newline-and-indent) (global-set-key "\C-o" 'kill-line) |
これを見ていくらかやりすぎだと思う人もいるでしょう。Vimへの未練も感じられます。僕自身、このキーバインドを他人に受け入れてもらえるとは思いません。ただ、そのキーバインドに僕自身が慣れているというだけの理由でそこに留めておくことはやめにしました。そして実際に使ってみて、僕はこのキーバインドの正しさを確信したのです。
今回は、僕がVimからEmacsに乗り換えるにあたって実際に行った設定を紹介します。Emacsから五メートル先の電子レンジを起動する方法を考えるようなのはやめにして、一度本当に重要なものは何かを見直す機会にしてみてはいかがでしょう。
## 効率的なカーソル移動
### カーソルのジャンプ
現実の世界で一からコードを書くことはそれほど多くはありません。最近Emacsでどんなファイルを見ましたか? それは十年前に書かれたPerl4のコードかもしれませんし、今自分が書いたばかりの、もっとクールでカッコだらけのプログラムかもしれません。いずれにしても多くは既存のコードの閲覧と修正です。そのためいかに素早くカーソルを目的の箇所に移動させ、修正するかが効率性の鍵となります。
どのように目的の場所にたどり着くか。最も単純なケースはバッファの行番号が明らかなときです。もしもエラーメッセージなどから移動すべき行番号がわかるのならば、M-x goto-lineのあとに行番号を指定すれば移動できます。デフォルトでgoto-lineはM-g gに割り当てられていますが、少し冗長です。代わりにM-gに変更すればわずかながら時間の節約になるでしょう(リスト2)。
1 2 |
- リスト2 行番号を指定して移動 (global-set-key "\M-g" 'goto-line) |
とはいえ、いくら毎時間エラーを起こしている僕でも、目的の行番号が明確なことばかりではありません。行番号はわからないまでも、指定箇所を探すためのキーワードを思いつくならばバッファ内検索が最善です。EmacsのC-sはデフォルトでインクリメンタル検索されるため、検索ワードを入力している間にも検索され、最初にヒットしたワードに飛ぶことができます。
また、C-sのあとにC-wすればカーソル位置にあるワードで検索することもできます。
### よりプリミティブな検索
しかし、本当にこれで十分でしょうか? C-sは検索としては十分な単純さだとは思いますが、カーソルを任意の場所に移動させたいときには冗長なように思います。
たとえば、以下のような文章があったとします。
1 |
Melos was furious. And he determined to eliminate that cunning and violent king. |
行頭にカーソルがあるとして、このfuriousをrejoicingに変更したいときはどうしますか? C-sを使うとすると、以下のような動作になります。
1 2 3 4 5 |
1. C-sでインクリメンタル検索を起動 2. fと入力 3. Return (カーソルはuの上) 4. C-bで1文字戻る 5. M-dで単語削除 |
そしてようやくrejoicingと入力します。たったこれだけの動作に5ストローク必要です。1ストローク0.2秒だとしても編集までに1秒ほどかかってしまいます。
慣れたEmacserの方はM-fを使えばいいと言うかもしれません。確かに単語移動すれば4ストロークで編集できます。けれど、この方法には汎用性がありません。もしdeterminedをgave upに変更したいときには7ストロークかかってしまいます。僕がしたいのはカーソルをどこに移動させるかをEmacsに伝えることであって、カーソルがその位置にくるまでキーを連打することではありません。
移動すべき場所が明確な場合、よりプリミティブな検索を使えばストロークを減らせます。僕はこの目的のためにリスト3の2つのコマンドを定義しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
- リスト3 一文字検索コマンド ;; これらの変数については後述 (defvar last-search-char nil) (defvar last-search-direction 'forward) ;; 一文字検索(順方向) (defun search-forward-with-char (char) (interactive "cMove to Char: ") (if (eq (char-after (point)) char) (forward-char)) (and (search-forward (char-to-string char) nil t) (backward-char)) (setq last-search-char char last-search-direction 'forward)) ;; 一文字検索(逆方向) (defun search-backward-with-char (char) (interactive "cMove backward to Char: ") (search-backward (char-to-string char) nil t) (setq last-search-char char last-search-direction 'backward)) (global-set-key "\C-f" 'search-forward-with-char) (global-set-key "\C-b" 'search-backward-with-char) |
これらはEmacsで一文字検索をするためのコマンドです。実行すると「Move to Char:」というプロンプトが表示され、何か文字を入力すると即座に最も近いその文字に移動します。Returnすら必要ありません。僕はこのコマンドをC-f、C-bに割り当てました。
すると先ほどの文字の編集は以下の3ストロークで済みます。
1 2 3 |
1. C-f 2. f 3. M-d |
ただしこの一文字検索は、実行は確かに早いのですが検索のヒット数が増えてしまうという欠点があります。たとえば上の例文で、determinedに移動するためにC-f dとすると、目的のdeterminedではなくAndのdにカーソルが移動してしまいます。目的の場所にたどり着くにはさらにC-f dしなければならず、2ストローク余計にかかることになります。これは手間です。
そこでリスト4のコマンドをさらに定義して、最後に実行した検索文字を再検索できるようにしました。
1 2 3 4 5 6 7 8 9 |
- リスト4 最後に実行した一文字検索を実行 (defun search-repeat-with-char () (interactive) (cond ((eq nil last-search-char) (message "You haven't searched yet. Stupid!")) ((eq last-search-direction 'forward) (or (search-forward-with-char last-search-char 2) (backward-char))) ((eq last-search-direction 'backward) (search-backward-with-char last-search-char)))) (global-set-key (kbd "C-;") 'search-repeat-with-char) |
リスト3で定義してあったlast-search-char、last-search-directionに直前の一文字検索の情報を保持しておき、それを再度実行するコマンドです。これをC-;などに割り当てれば順にヒットする箇所を辿ることができます。
これらを組み合わせ、インクリメンタル検索や単語移動と使い分ければより効率的にカーソルを意図した位置に移動させることができます。
### マークによる移動
これからバッファ内を探検したいけれど、またあとで今の場所に戻ってきたいなぁと思うことはよくあります。そのとき、今の位置をEmacsに覚えさせておければ安心して歩き回れるでしょう。
位置をEmacsに覚えさせる方法はいくつかありますが、最も簡単なのは、範囲選択で使用されるC-SPCでマークをつけることです(注1)。
注1)transient-mark-modeがtになっている場合、カーソルを移動させると反転されてしまうので、単にマークをつけるだけであればC-SPC C-SPCと2回入力する必要があります。
マークした箇所にカーソルを移動させるexchange-point-and-markというコマンドがありますが、これは強制的に範囲選択を有効にしてしまうため目に優しくありません。そこで僕はリスト5のコマンドを新たに定義しました。
1 2 3 4 5 6 7 |
- リスト5 最後のマークに移動 (defun move-to-mark () (interactive) (let ((pos (point))) (goto-char (mark)) (push-mark pos))) (global-set-key "\C-p" 'move-to-mark) |
これでマークをつけてバッファ内を歩きまわったのち、C-pするだけで元の位置に戻れます。
### ウィンドウ内カーソル移動
VimにあってEmacsにないキーのうち、Emacsにも欲しいと思うものの代表に、H, M, Lがあります。これらはそれぞれ、画面の表示範囲の一番上、真ん中、一番下に移動するコマンドです。こんなものが必要になるときがあるのかと疑問に思う人もいるかもしれません。しかし、コードの構造を意識せず直感的に画面の上とか下にカーソルをジャンプできるのはときに魅力的です。
幸いなことにEmacsにこの機能を実装するのは難しくありません。リスト6のように設定すればC-M-h, C-M-m, C-M-lで表示範囲を飛び回ることができます。
1 2 3 4 |
- リスト6 ウィンドウ内のカーソル移動 (global-set-key (kbd "C-M-h") (lambda () (interactive) (move-to-window-line 0))) (global-set-key (kbd "C-M-m") (lambda () (interactive) (move-to-window-line nil))) (global-set-key (kbd "C-M-l") (lambda () (interactive) (move-to-window-line -1))) |
### ウィンドウ間の移動
ここまで一つのバッファを動き回るという前提で話をしていましたが、大抵プログラムしているときには多くのバッファが開かれており、画面はいくつかに分割され、カーソルはその間を行ったり来たりします。
Emacsではウィンドウ間の移動に、主にC-x oを使いますが、これでは一方向のウィンドウ移動しかできません。一つ前のウィンドウに戻るためにはぐるりとウィンドウを一周しなければいけません。さらにこれも表示順ではなく開かれた順なので、ウィンドウ番号を意識しておかなければなりません。これを意識せず視覚的に上下左右に移動できればずっと楽になるでしょう。
僕はリスト7のような一連のキーを設定しました。ウィンドウ間の移動のために新しくプリフィックスキーとしてC-qを登録し、C-q hで左、C-q jで下のウィンドウ、といった具合にカーソルを移動できます。
1 2 3 4 5 6 7 |
- リスト7 分割したウィンドウ間を移動 (define-prefix-command 'windmove-map) (global-set-key (kbd "C-q") 'windmove-map) (define-key windmove-map "h" 'windmove-left) (define-key windmove-map "j" 'windmove-down) (define-key windmove-map "k" 'windmove-up) (define-key windmove-map "l" 'windmove-right) |
少々蛇足になりますが、ウィンドウ操作のついで画面分割についても改善しておきましょう。画面は場合に応じて縦に分割したり横に分割したりするものですが、その多くはウィンドウ幅の広い方向に分割します。横長の画面ならばC-x 3、縦長ならばC-x 2といった具合に使い分けるでしょう。
大したオーバーヘッドではありませんが、もしウィンドウ幅を意識せずに分割できるならばそれに越したことはありません(リスト8)。
1 2 3 4 5 6 7 |
- リスト8 縦横幅に応じて画面を分割 (defun split-window-conditional () (interactive) (if (> (* (window-height) 2) (window-width)) (split-window-vertically) (split-window-horizontally))) (define-key windmove-map "s" 'split-window-conditional) |
C-q sを押せばウィンドウサイズに応じて縦や横に分割してくれます(注2)。新しいウィンドウが欲しいと思えば、ただC-q sをすればいいのです。わざわざ画面幅を気にする必要はありません。
注2)環境によっては縦横幅を正しく比較できない場合があります。
## 効率的なテキスト編集
### 前の単語を削除
Emacsでキーにコマンドを割り当てていくと、だんだん空いているキーがなくなってきます。そのとき、ある条件下でしか使えないキーを使って、エラーメッセージを表示する以外にもっと有用な動作を割り当ててやればキーの節約になります。
たとえば、C-wにはkill-regionが割り当てられており、範囲を選択中しか動作しません。もし範囲が選択されていないときにはbackward-kill-wordの挙動をするようになればずっと有用です(リスト9)。これはzshなどの他のソフトウェアのショートカットキーと同じ挙動なのでわかりやすいでしょう。
1 2 3 4 5 6 7 8 |
- リスト9 範囲指定していないとき、C-wで前の単語を削除 (defadvice kill-region (around kill-word-or-kill-region activate) (if (and (interactive-p) transient-mark-mode (not mark-active)) (backward-kill-word 1) ad-do-it)) ;; minibuffer用 (define-key minibuffer-local-completion-map "\C-w" 'backward-kill-word) |
これで、C-wは前の単語削除にも使えるようになります。
### カーソル位置の単語を削除
C-wにbackward-kill-wordを割り当ててみると、意外と単語を削除して書き直すという場面が多いことに気づきました。とくに単語削除の前に一度M-fで単語の末尾に移動してからC-wすることが多いようです。僕が本当にやりたいことは、カーソルが乗っている単語を判定して削除する機能であり、単語の末尾に移動することではありません。
そこでリスト10ようなコマンドを定義しました。
1 2 3 4 5 6 7 8 9 |
- リスト10 カーソル位置の単語を削除 (defun kill-word-at-point () (interactive) (let ((char (char-to-string (char-after (point))))) (cond ((string= " " char) (delete-horizontal-space)) ((string-match "[\t\n -@\[-`{-~]" char) (kill-word 1)) (t (forward-char) (backward-word) (kill-word 1))))) (global-set-key "\M-d" 'kill-word-at-point) |
Emacs Lispには単語の境界かどうかを判定するコマンドがないため、backward-wordを使って移動しています。ただこれでは単語の先頭にいるときに前の単語にまで飛んでしまうので、一度forward-charしてからbackward-wordしています。これでM-dは、カーソルが単語のどこにあろうと、カーソル位置の単語を削除できるコマンドになりました。
また、単語を削除するという目的とは異なるのですが、このkill-word-at-pointは半角スペースの上にあるときに異なる挙動をします。通常のkill-wordならば半角スペースを無視して次の単語境界までを削除しますが、これを意図して使うことはほとんどないでしょう。そこで、半角スペースがある場合は代わりにdelete-horizontal-spaceを実行するようにしました。これで連続する半角スペースをM-dで削除できます。
## おわりに
僕は以上のすべての内容を一気に設計したわけではありません。自分が日常の作業をしているとき、操作に違和感を覚えたときに随時加え、試して、場合によっては書き直して構築したものです。
「はじめに」で述べたように、これらがすべて万人に受け入れられる設定ではないでしょう。本当にエディタを自分に最適化させるには、やはり自分自身で再設計をする必要があります。そのためには日常のキー操作に常に敏感でなければなりません。自分の無意識のキー入力の一つ一つに注目し、繰り返し入力、冗長さ、打ち間違いをいかに減らすかに神経を尖らせておくのです。
たとえば、僕は行を連結させる際、kill-lineしたあとに毎回インデントを削除する作業をしていました。これを自動化させるにはどうすればいいでしょう。そこでリスト11のようなコードを書きました。
1 2 3 4 5 6 |
- リスト11 kill-lineで行が連結したときにインデントを減らす (defadvice kill-line (before kill-line-and-fixup activate) (when (and (not (bolp)) (eolp)) (forward-char) (fixup-whitespace) (backward-char))) |
これは行末のkill-lineで行を連結させたあと、行の先頭のインデントをなくすという一連の決まり入力の冗長さを廃するものです。
Emacsを選ぶ理由として「Vimにできないことができる」ではなく、自信を持って効率的なテキスト編集を掲げられるようになりたいものですね。
最近のコメント