Emacs で Helm をストレスなく使うための個人的な設定

普段の開発で Emacs を使っているんですが、現時点で MELPA で anything にヒットするパッケージ数が 5、helm にヒットするパッケージ数が 49 ということを考慮すると、そろそろ helm に移行しないと時代に取り残される感がしてきます。

というわけで、ようやく最近移行してみたんですが、デフォルトの設定だとストレスで発狂しそうでした。ストレスなく使えるようにするまでの移行コストがバカにならなかったので、現時点の設定を公開しておきます。
Emacs を快適に使うための Helm の設定ではなく、あくまでストレスなく使うための設定です。
これから移行する方の参考になれば幸いです。
※最新版はこちら→ Emacs で Helm v1.8.0 をストレスなく使うための個人的な設定 - あらびき日記

ちなみに自分は Anything をほとんど使いこなしてなかった人だと思います。
どれだけ使いこなしてなかったかというと、2011 年の 8 月に auto-install 経由でインストールして次の設定を書いただけです。

(require 'anything-startup nil t)

これだけで、C-x b (switch-to-buffer)、M-x (execute-extended-command)、imenu の操作性が劇的に改善されて感動したものです。他のコマンドに関してはほとんど Anything の恩恵を受けていないはずです。
なので、Anything を使い倒していた人にとってはあまり参考にならないかもしれません。

環境

  • Mac OS X 10.9.1
  • iTerm2 1.0.0.20131228
  • Emacs 24.3.1(Homebrew で –cocoa オプション付きでインストールしたものだが基本 -nw で使用)
  • Helm 20140102.628(package.el で MELPA からインストール)

ストレスレスな設定

次のような設定をすることで、とりあえずストレスなく使えるようになった気がします。

※ v1.8.0 に対応した設定はこちら → Helm v1.8.0 をストレスなく使うための個人的な設定

(el-get-bundle "abicky/helm" :branch "develop/v1.8.0"
  :build (("make"))
  :build/darwin `(("make" ,(format "EMACS_COMMAND=%s" el-get-emacs)))
  (require 'helm-config)
  (helm-mode 1)
  (define-key global-map (kbd "M-x")     'helm-M-x)
  (define-key global-map (kbd "C-x C-f") 'helm-find-files)
  (define-key global-map (kbd "C-x C-r") 'helm-recentf)
  (define-key global-map (kbd "M-y")     'helm-show-kill-ring)
  (define-key global-map (kbd "C-c i")   'helm-imenu)
  (define-key global-map (kbd "C-x b")   'helm-buffers-list)
  (define-key global-map (kbd "M-r")     'helm-resume)
  (define-key global-map (kbd "C-M-h")   'helm-apropos)
  (define-key helm-map (kbd "C-h") 'delete-backward-char)
  (define-key helm-find-files-map (kbd "C-h") 'delete-backward-char)
  (define-key helm-find-files-map (kbd "TAB") 'helm-execute-persistent-action)
  (define-key helm-read-file-map (kbd "TAB") 'helm-execute-persistent-action)

  ;; Disable helm in some functions
  ;; (add-to-list 'helm-completing-read-handlers-alist '(find-file . nil))
  ;; (add-to-list 'helm-completing-read-handlers-alist '(write-file . nil))
  (add-to-list 'helm-completing-read-handlers-alist '(find-alternate-file . nil))
  (add-to-list 'helm-completing-read-handlers-alist '(find-tag . nil))

  (setq helm-buffer-details-flag nil)

  ;; Emulate `kill-line' in helm minibuffer
  (setq helm-delete-minibuffer-contents-from-point t)
  (defadvice helm-delete-minibuffer-contents (before emulate-kill-line activate)
    "Emulate `kill-line' in helm minibuffer"
    (kill-new (buffer-substring (point) (field-end))))

  (defadvice helm-ff-kill-or-find-buffer-fname (around execute-only-if-file-exist activate)
    "Execute command only if CANDIDATE exists"
    (when (file-exists-p candidate)
      ad-do-it))

  (setq helm-ff-fuzzy-matching nil)
  (defadvice helm-ff--transform-pattern-for-completion (around my-transform activate)
    "Transform the pattern to reflect my intention"
    (let* ((pattern (ad-get-arg 0))
           (input-pattern (file-name-nondirectory pattern))
           (dirname (file-name-directory pattern)))
      (setq input-pattern (replace-regexp-in-string "\\." "\\\\." input-pattern))
      (setq ad-return-value
            (concat dirname
                    (if (string-match "^\\^" input-pattern)
                        ;; '^' is a pattern for basename
                        ;; and not required because the directory name is prepended
                        (substring input-pattern 1)
                      (concat ".*" input-pattern))))))

  (defun helm-buffers-list-pattern-transformer (pattern)
    (if (equal pattern "")
        pattern
      (let* ((first-char (substring pattern 0 1))
             (pattern (cond ((equal first-char "*")
                             (concat " " pattern))
                            ((equal first-char "=")
                             (concat "*" (substring pattern 1)))
                            (t
                             pattern))))
        ;; Escape some characters
        (setq pattern (replace-regexp-in-string "\\." "\\\\." pattern))
        (setq pattern (replace-regexp-in-string "\\*" "\\\\*" pattern))
        pattern)))


  (unless helm-source-buffers-list
    (setq helm-source-buffers-list
          (helm-make-source "Buffers" 'helm-source-buffers)))
  (add-to-list 'helm-source-buffers-list
               '(pattern-transformer helm-buffers-list-pattern-transformer))

  (defadvice helm-ff-sort-candidates (around no-sort activate)
    "Don't sort candidates in a confusing order!"
    (setq ad-return-value (ad-get-arg 0)))
  )

ストレスだった点と対処方法

no window system だと minibuffer でカーソルが表示されない

かなりクリティカルな問題なので環境依存な気がしますが、次のパッチを当てることで解決しました。
https://github.com/abicky/.emacs.d/blob/9264a7e/patch/helm/helm.el.patch

– 2014-01-18 追記

よく見たらとんでもない patch になってました。正確には次の変更で直ります。
https://github.com/abicky/helm/commit/20018bc

minibuffer で C-h が効かない

global-map の C-h を delete-backward-char に割り当てているんですが、Helm では prefix command として割り当てられているためこれが効かなくて辛かったです。
デバッグ用途で割り当てられているようなので上書きしてしまいます。

(define-key helm-map (kbd "C-h") 'delete-backward-char)
(define-key helm-find-files-map (kbd "C-h") 'delete-backward-char)

minibuffer で C-k を押すと先頭から削除されるし kill ring にも追加されない

C-k で問答無用に先頭から削除されてしまうのは物凄く違和感がありました。C-a C-k をすれば済む話なのに、現在位置から末尾まで削除する機能がなくなるのは辛いです。
また、現在のファイルパスをコピーしたりするために minibuffer で kill-line してコピーすることがよくありましたが、Helm では kill ring に追加されません。

これに対処するのが次の設定です。

;; Emulate `kill-line' in helm minibuffer
(setq helm-delete-minibuffer-contents-from-point t)
(defadvice helm-delete-minibuffer-contents (before helm-emulate-kill-line activate)
  "Emulate `kill-line' in helm minibuffer"
  (kill-new (buffer-substring (point) (field-end))))

C-x C-v C-k C-g で現在のファイル名をコピーしたい

自分の中で C-x C-v C-k C-g が現在のファイル名のコピーということで馴染んでしまっていたんですが、Helm のインタフェースだと preselect に現在のファイル名が渡るだけで minibuffer には出てきません。

Helm では read-file-name-function を helm-generic-read-file-name、completing-read-function を helm-completing-read-default に設定することで Helm のインタフェースを使えるようにしていますが、helm-completing-read-handlers-alist に関数名と nil のペアを指定することで read-file-name-default を使うようにできます。

というわけで find-alternate-file を登録しておきます。

;; Disable helm in some functions
(add-to-list 'helm-completing-read-handlers-alist '(find-alternate-file . nil))

find-file や write-file のインタフェースが気に食わない場合は次の内容も追加すると良いと思います。

(add-to-list 'helm-completing-read-handlers-alist '(find-file . nil))
(add-to-list 'helm-completing-read-handlers-alist '(write-file . nil))

C-x C-f でタブ補完(選択)できない

Helm はタブに helm-select-action が割り当てられているので、基本的にファイル名を補完する目的でタブを押しても意味がありません。
タブでファイル名を補完したい場合はタブに helm-execute-persistent-action(C-z を押した時に実行されるコマンド)を割り当てておきます。

;; For find-file etc.
(define-key helm-read-file-map (kbd "TAB") 'helm-execute-persistent-action)
;; For helm-find-files etc.
(define-key helm-find-files-map (kbd "TAB") 'helm-execute-persistent-action)

helm-find-files に関しては多彩なアクションが割り当てられているはず1なので helm-select-action は何らかのキーに割り当てておくと良いかもしれません。

デフォルトだと M-x や C-x b の補完候補が微妙

デフォルトだと M-x を押した時のコマンドの並び順が酷く、C-x b を押した時は新しい buffer を作成するためか、既存 buffer の選択候補が 1 つしかなくてもいちいちカーソルを移動して選択しなければならなくかなりストレスでした。

Helm では特定のコマンドに対しては代替用のコマンドを個別に用意しているので、それらを使うと快適になります。

M-x には helm-M-x を割り当て、C-x b には helm-buffers-list を割り当てることでストレスがだいぶ減りました。
C-x C-f は helm-find-files を使うか Helm を無効化するか悩ましいところですが、とりあえずしばらく helm-find-files を割り当てておこうと思います。

(define-key global-map (kbd "M-x")     'helm-M-x)
(define-key global-map (kbd "C-x C-f") 'helm-find-files)
(define-key global-map (kbd "C-x C-r") 'helm-recentf)
(define-key global-map (kbd "M-y")     'helm-show-kill-ring)
(define-key global-map (kbd "C-c i")   'helm-imenu)
(define-key global-map (kbd "C-x b")   'helm-buffers-list)

helm-find-files でタブを 2 回押すと新しいファイル用のバッファが作成される

タブでファイル名を補完できるようにしたことの弊害なんですが、「このファイルが存在するはず!」と思って少し文字を入力してタブを押すと、ファイルが存在しない場合に新しいファイル用のバッファが作成されます。
タブだと押しやすいせいでこのような誤操作をしてしまうわけですが、そもそもこの機能は既存のファイルの中身を確認するためにあるべきものだと思います。

というわけで、ファイルが存在しない場合は何もしないように advice を定義しました。

(defadvice helm-ff-kill-or-find-buffer-fname (around execute-only-if-exist activate)
  "Execute command only if CANDIDATE exists"
  (when (file-exists-p candidate)
    ad-do-it))

候補のフィルタリングのロジックがストレス

helm-find-files や helm-buffers-list はフィルタリングのロジックが複雑です。起動している間に C-c ? を押してヘルプを確認してようやく理解できます。

例えば、ホームディレクトリ以下で helm-find-files を実行し、.e を入力したとすると、最初にヒットするのが自分の環境では .gem です。これは “\.e” という正規表現から “.*.e” という正規表現に変換されるからです。
諸悪の根源は helm-ff-transform-fname-for-completion という関数で、ido-mode で ido-enable-flex-matching を t にした時の挙動に似たものになるよう正規表現を変換しているみたいです。
というわけで、これを無効化する advice を定義しました。

(defadvice helm-ff-transform-fname-for-completion (around my-transform activate)
  "Transform the pattern to reflect my intention"
  (let* ((pattern (ad-get-arg 0))
         (input-pattern (file-name-nondirectory pattern))
         (dirname (file-name-directory pattern)))
    (setq input-pattern (replace-regexp-in-string "\\." "\\\\." input-pattern))
    (setq ad-return-value
          (concat dirname
                  (if (string-match "^\\^" input-pattern)
                      ;; '^' is a pattern for basename
                      ;; and not required because the directory name is prepended
                      (substring input-pattern 1)
                    (concat ".*" input-pattern))))))

ファイル名をフィルタリングする場合、多くの場合 “.” を dot 1文字とマッチしてほしいと思うのでエスケープする処理も入っています。

helm-ff-smart-completion を nil にすることで余計なお世話がなくなりそうですが、スペースが含まれると意味がないのと、prefix match になるのでそれなら find-file の方がマシです。

また、helm-buffers-list の場合、例えば scratch buffer を選択しようとして s を入力すると候補に挙がるのに、sc を入力すると候補から消えてしまいます。
これは helm-buffers-list において prefix の * が buffer の メジャーモード名に対するフィルタリングを意味しているからです。s だと lisp-interaction-mode である *scratch buffer は候補に残るけど、*sc だと メジャーモード名が sc にマッチしないので候補から落ちます。
詳細は C-c ? でヘルプを確認してください。

helm-find-files と違って、理解してしまえば * を最初に入力しなければいいだけですが、temporary buffer などの内容を確認する際は * を入力して即フィルタリングしたいです。
というわけで、先頭に * を入力してもそのまま扱われ、メジャーモード名でフィルタリングしたい場合は prefix として = を入力できるよう helm-source-buffers-list に pattern-transformer を設定しました。
これによって入力したパターンが変換されます。

(defun helm-buffers-list-pattern-transformer (pattern)
  (if (equal pattern "")
      pattern
    ;; Escape '.' to match '.' instead of an arbitrary character
    (setq pattern (replace-regexp-in-string "\\." "\\\\." pattern))
    (let ((first-char (substring pattern 0 1)))
      (cond ((equal first-char "*")
             (concat " " pattern))
            ((equal first-char "=")
             (concat "*" (substring pattern 1)))
            (t
             pattern)))))

(add-to-list 'helm-source-buffers-list
             '(pattern-transformer helm-buffers-list-pattern-transformer))

以上です!
細々したところはまだ多少ストレスがありますが、とりあえずこれだけ設定すれば個人的には満足です。Anything と比べてストレスなく使えるという点では。

そのうち Helm を使い倒すための設定についても書きたいですね。

2014-01-25 追記

続編書きました
Helm をストレスなく使うための個人的な設定 (2) - あらびき日記

  1. 自分の環境だと何故かアクションが1つも表示されないんですが・・・