機能とコマンドとキーバインド : Emacs Advent Calendar jp: 2009

Emacs Advent Calendar jp: 2009 参加記事です。昨日は id:rubikitch さんでした。明日は id:hayamiz さんです。今回の記事では機能とその使い方について、自分のこれまでの経験を少しご紹介したいと思います。

はじめに

Emacsとは気づけば長いつきあいで、大学生の時からもう14年ほど使っています。今でもいちユーザーの立場でほそぼそと使っていますが、常に発見の連続で、懐の深さに感動している毎日です。

なんと言ってもEmacsの醍醐味は、両手の前に広がる際限のない拡張性です。EmacsWikiやMeadowMemoを見ていると、知らなかった新しい機能がごろごろしていて、そんなEmacsLispを発見するととても楽しくなります。

大量の機能とどうつきあうか

しかしながら、新しい機能を入れたときにいつも悩むのは、どうやってその機能を呼び出すかです。ファイルに関連づけられたModeならまだ良いのですが、キーバインドを覚えなければいけなかったり、自分で割り当てたりする必要があるものは、後ですぐ忘れてしまいます。そのうち存在さえも忘れてしまって、.emacsの肥やしとなってしまいます。自分はそんなことがしょっちゅうです。

また、追加した機能だけでなく、最初からEmacsに入っているような機能でさえ、大量すぎて覚えきれません。そもそも存在さえ知らないんだけども、でも非常に便利なコマンドがたくさんあります。

これらの大量の機能について、どのようにつきあっていけばいいか、自分の経験を元にまとめてみました。

大量の機能のユースケースとして「探す、(機能の存在に)気づく」という場面と、「機能を呼び出す」という場面があります。必要な機能を探しつつ目的のコマンドを特定する作業が前者で、コマンドが特定されていて呼び出す作業をいかに効率化するかが後者になると思っています。

話をまとめた図

https://cacoo.com/diagrams/4wJhgJeNCUM1Xsiv

機能を探す、気づく

探す・気づくという場面では、欲しい機能が特定されていません。この場面は、逆引き的に機能を探しているか、特に必要とされていないけども機能を閲覧しているという状況です。この状況では、アイコンや一覧を表示して選ぶ方法か、補完や検索で高速に対象を特定していくというインタフェースがあるようです。

標準メニュー、コマンド一覧

機能を一覧する方法の一番簡単な方法は「メニュー」です。Emacsの画面の上部に常に表示されいるため、いつでも機能を探すことができます。また、キーバインド(ショートカット)も表示されているため、そこから機能や呼び出し方を覚えることもできます。メニューは操作は簡単ですが、高速に操作することが出来ません。自分はメニューを使う機会がほとんど無かったので、今はメニューバーを消しています。

他には、現在のキーバインドを一覧する「describe-binding」コマンドもあります。ただ、これをわざわざ呼び出して機能を探すのは難しいです。自分は、後述する anything を使った descbinds-anything.elを使っています。

補完と検索

機能を探すもう一つの方法は補完や検索です。これらは、頻繁に使わないような機能を絞り込みによって特定するインタフェースです。コマンドの存在を知っているか、もしくはコマンドを絞り込むキーワードを知っている必要がありますが、頻繁には使わないコマンドを呼び出すときに大変便利です。

補完は icicles.el という拡張が非常に有名です。各種補完を強力に行うことが出来ます。ただ、anything を使うようになってから completion-read をあまり使わないので、自分は使ってません。

コマンドの検索はなんといっても anything.el です。コマンドだけでなく、いろいろなものを一気に列挙して検索して高速に絞り込むことが出来ます。ここは、ぜひ昨日の rubikitch さんの記事を参考にしてインストールして使ってみてください。

カスタマイズや工夫次第ですごく便利なツールになります。ぜひ、各地の導入記事を見ながら活用してみてください。自分のところでは関数・マクロを調べたりinfoを調べるElispを作っています。また、自分が作るアプリケーションのインタフェースもほとんど anything を使っています。

機能を呼び出す

これまではうろ覚えの機能を探して呼び出すという場面でしたが、今度は特定された機能をどう呼び出すかという場面です。必要なコマンドはすでに特定されていますので、そのコマンドをどうやって呼び出すか、具体的にはどのキーにバインドするかという話になります。

キーの割り当てには、以下のようなよく知られた制限があります。

  • キーバインドは見えないので、覚えてないと使えない。存在自体も分からない。
  • よく使うキーは覚えるが、ほとんど使わないキーは覚えられない。
  • キーの数は有限で、しかも良くあるキーはすでに割り当てられている。

これらをどう克服するかがポイントになります。

キーバインドを可視化する : one-key.el

まず、キーバインドを可視化するものとして、キーバインドを可視化する「one-key.el」があります。one-key.elは、どのキーを押せばどんな機能になるかが可視化されるため、まだキーを覚えられない初心者やうろ覚えの機能を呼び出すときに使えます。

Emacs 初心者必見! one-key.el はキーバインドが覚えられない人への特効薬だ

キー数を増やす : key-chord.el

次に、キーが有限であることを克服するために、同時押しによるキー数の増加をねらったものが「key-chord.el」です。Emacsではすでにほとんどのキーにコマンドが割り当てられているため、実際には自分で自由にキーバインドを割り当てることが難しいのですが、この key-chord.el を使うことによって、既にあるキーバインドとかぶらないように新しい機能を割り当てていくことが出来ます。同時押しというよりは、キーとキーの隙間を新たなキーとして定義する発想が非常に新しいと思いました。自分のところでも使ってはいますが、間違えて発動して混乱してしまうことがあったり、やっぱりそれほど使わない機能は anything で探した方が早いので、たくさんは割り当てていません。

Ctrl+中指か薬指を使うキー操作が多いEmacsで指の負担を軽くする方法

キー入力の履歴依存コマンド : smartchr.el

さて、これまでのキー割り当ては、単純にキーに単機能を割り当てていくというものでしたが、それでは機能の数に応じて覚えなければならないキーバインドが増えていくため、使いこなすためには大変な訓練が必要になります。そこで、バッファへの入力内容やキー入力の履歴(コンテキスト)を見ながら、同じキーでも期待した機能を選んで発揮するようにすれば、覚えるキーが少なくてすみますので効率が良くなります。このようなコンテキスト依存の機能というのは空気を読むということですので、うまく割り当てられれば非常に便利になると思っています。

まず、キー入力の履歴をつかって入力する内容や、発動する機能を切り替えるという拡張が「smartchr.el」です。詳しくは以下のサイトを見てください。非常に便利ですので、自分のところでもいくつか割り当てて使っています。

smartchr.elを使って生産性を上げる

コンテキスト依存コマンド

最後に、自分が使っているコンテキスト依存のキー割り当てをいくつか紹介します。自分の感覚で仕込んでいますので、皆さんにとって便利かどうかは分かりませんが、一つの参考として見てもらえればと思います。(すいません。うちの .emacs から抜き出したのでそのままでは動かないかもしれません!)

;; Elscreenにて、スクリーンが一つであればバッファを切り替える
;; スクリーンが複数あればスクリーンを切り替える。

(defun my-elscreen-next ()
  "If the elscreen of a frame has only one screen, this command
executes `next-buffer'."
   (interactive)
   (if (= 1 (elscreen-get-number-of-screens))
       (next-buffer)
     (elscreen-next)))

(defun my-elscreen-prev ()
  "If the elscreen of a frame has only one screen, this command
executes `previous-buffer'."
   (interactive)
   (if (= 1 (elscreen-get-number-of-screens))
       (previous-buffer)
     (elscreen-previous)))

(global-set-key [?\C-,] 'my-elscreen-prev)
(global-set-key [?\C-.] 'my-elscreen-next)

;; リージョンが活性化していればリージョン削除
;; 非活性であれば、直前の単語を削除

(defun kill-region-or-backward-kill-word ()
  (interactive)
  (if (region-active-p)
      (kill-region (point) (mark))
    (backward-kill-word 1)))

(global-set-key "\C-w" 'kill-region-or-backward-kill-word)

;; カーソール位置前後が空白であれば空白削除
;; 空白でなければ単語削除

(defun kill-word-or-delete-horizontal-space (arg)
  (interactive "p")
  (let ((pos (point)))
    (if (and (not (eobp))
             (= (char-syntax (char-after pos)) 32)
             (= (char-syntax (char-after (1+ pos))) 32))
        (prog1 (delete-horizontal-space) 
          (unless (memq (char-after pos) '(?( ?) ?{ ?} ?[ ?]))
            (insert " ")))
      (kill-word arg))))

(global-set-key (kbd "M-d") 'kill-word-or-delete-horizontal-space)

;; カーソール位置が"や'のクオートであれば、クオート文字を取り替える
;; それ以外であればカーソール前後の文字を取り替える

(defvar swap-quotes-list
  '((?\" ?\') (?\' ?\`) (?\` ?\")))

(defun transpose-chars-or-swap-quotes (arg)
  (interactive "p")
  (or (swap-quotes) (transpose-chars arg)))

(defun swap-quotes ()
  (interactive)
  (catch 'break
    (dolist (i swap-quotes-list)
      (let ((target-char (car i))
            (replaced-char (cadr i))
            (prev-pos (point)))
        (if (= (char-after (point)) target-char)
            (save-excursion
              (forward-char 1)
              (let ((next-pos (re-search-forward (char-to-string target-char))))
                (if next-pos
                    (subst-char-in-region prev-pos next-pos target-char replaced-char)
                  (message "The corresponding quote is not found.")))
              (throw 'break t)))))
    nil))

(global-set-key "\C-t" 'transpose-chars-or-swap-quotes)

終わり

いかがだったでしょうか。他の皆さんみたいに、かっこいい elisp ではありませんでしたが、皆さんのユーザーインタフェース改善のアイデアになればと思っています。

ほとんどのリファレンスが rubikitch さんになってしまいました。いつも大変お世話になっております!

最後に、このようなユーザーインタフェースへの考察やアイデアの議論を tenjin.web にて行っていますので、もし興味がありましたら是非参加してみてください。