helm-filelist

環境をほったらかしている間に、いつのまにか Emacs24 がリリースされてたり anything が fork して helm に変ってたりしてたので、Emacs 本体やら elisp やらをアップデートして、anything も helm に移行。

helm へ移行したものの、anything-filelist 相当のものが見つからない。なくてもなんとかなるけど、ないと微妙に不便を感じてしまう。どこかにあるのかもしれないけど見つけられないので勉強とリハビリがてら自分で作ってみた。さすがに grep-candidate 属性をまるごと実装する余裕はないので helm-filelist がとりあえず動けばいい程度に実装。

(eval-when-compile (require 'cl))
(require 'helm)
(require 'helm-files)

(defvar helm-filelist-file-name "/tmp/all.filelist")
(defvar helm-filelist-case-fold-search helm-case-fold-search)
(defvar helm-filelist-grep-command "LANG=C grep")
(defvar helm-filelist-candidate-number-limit 200)
(defvar helm-filelist-async t)

(defun helm-filelist-split-pattern (patterns)
  (delq "" (split-string patterns " ")))

(defun helm-filelist-make-grep-cmd (pattern file ignore-case)
  (format "%s %s %s %s 2>/dev/null"
          helm-filelist-grep-command
          (if ignore-case "-i" "")
          (shell-quote-argument pattern)
          (if file (shell-quote-argument file) "")))

(defun helm-filelist-ignore-case-p (pattern)
  (case helm-filelist-case-fold-search
        (smart (let ((case-fold-search nil))
                 (not (string-match-p "[A-Z]" pattern))))
        (t (if helm-filelist-case-fold-search t nil))))

(defun helm-filelist-make-cmd-line (patterns file &optional limit)
  (with-temp-buffer
    (loop for pattern in (helm-filelist-split-pattern patterns)
          for ignore-case-flag = (helm-filelist-ignore-case-p pattern)
          for i from 0
          do
          (unless (zerop i) (insert " | "))
          (insert (helm-filelist-make-grep-cmd pattern
                                               (if (zerop i) file nil)
                                               ignore-case-flag)))
    (when limit (insert (format " | head -n %d" limit)))
    (buffer-string)))

(defun helm-filelist-init-async ()
  (let ((process-connection-type))
    (prog1
        (start-process-shell-command
         "filelist-grep-process" helm-buffer
         (helm-filelist-make-cmd-line helm-pattern
                                      helm-filelist-file-name
                                      helm-filelist-candidate-number-limit))
      (set-process-sentinel
       (get-process "filelist-grep-process")
       #'(lambda (process event)
           (unless (string= event "finished\n")
             (helm-log "Error: Filelist %s"
                       (replace-regexp-in-string "\n" "" event))))))))

(defun helm-filelist-init ()
  (split-string
   (shell-command-to-string
    (helm-filelist-make-cmd-line helm-pattern
                                 helm-filelist-file-name
                                 helm-filelist-candidate-number-limit))
   "\n"))

(defun helm-source-filelist ()
  `((name . "FileList")
    ,@(if helm-filelist-async
          '((candidates-process . helm-filelist-init-async)
            (delayed))
        '((candidates . helm-filelist-init)
          (volatile)))
    (type . file)
    (requires-pattern . 3)
    (history . ,'helm-file-name-history)
    (keymap . ,helm-generic-files-map)
    (helm-message . helm-generic-file-help-message)
    (candidate-number-limit . ,helm-filelist-candidate-number-limit)
    (mode-line . helm-generic-file-mode-line-string)))

(defun helm-filelist ()
  (interactive)
  (let ((helm-ff-transformer-show-only-basename nil))
    (helm-other-buffer (helm-source-filelist) "*helm filelist*")))

(defun helm-filelist+ ()
  (interactive)
  (let ((helm-ff-transformer-show-only-basename nil))
    (helm-other-buffer  `(helm-source-buffers-list
                          helm-source-recentf
                          helm-source-file-cache
                          helm-source-files-in-current-dir
                          ,(helm-source-filelist))
                        "*helm filelist+*")))

(provide 'helm-filelist)

導入

上の内容をファイル名 helm-filelist.el で保存して、ロードパスの通ったところにおき、(require 'helm-fielist) する。

設定

helm-filelist-file-name
ファイルリストのパス
helm-filelist-async
nil の場合、ファイルリストの grep 検索を同期実行する。t の場合は非同期実行。(anything-filelistでは、anything-c-filelist-file-name のパスが anything-grep-candidates-fast-directory-regexp正規表現にマッチしていた場合には同期実行になるはず)

コマンド

helm-filelist
情報源がファイルリストのみのコマンド
helm-filelist+
helm-for-files の情報源から locate をファイルリストに置き換えたコマンド

anything-filelist が遅い。

anything-filelist 〜すべてのファイルを瞬時に開く方法〜 - http://rubikitch.com/に移転しましたを参考に設定してるんだけど重いというか候補の表示に時間がかかる。anything-grep-candidates-fast-directory-regexpを設定すると候補を表示するまで固まってしまう。このままだと使いずらいのでなんとかしたいと思ってなんとかしてみた。

ロケールを C にする

そういえば grep はマルチバイト文字を扱うロケールだと遅くなるというのをどこかで聞いたことがあるなと思ったので試してみる。ちなみに今のロケールの設定はこんなかんじ。

[mtym]% locale                                                     (~)
LANG=ja_JP.utf8
LC_CTYPE="ja_JP.utf8"
LC_NUMERIC="ja_JP.utf8"
LC_TIME="ja_JP.utf8"
LC_COLLATE="ja_JP.utf8"
LC_MONETARY="ja_JP.utf8"
LC_MESSAGES="ja_JP.utf8"
LC_PAPER="ja_JP.utf8"
LC_NAME="ja_JP.utf8"
LC_ADDRESS="ja_JP.utf8"
LC_TELEPHONE="ja_JP.utf8"
LC_MEASUREMENT="ja_JP.utf8"
LC_IDENTIFICATION="ja_JP.utf8"
LC_ALL=


まずは今の設定のままでの grep 所要時間。

[mtym]% time grep -ih anything /tmp/anything-filelist.all.filelist | head -n 200 > /dev/null
grep -ih anything /tmp/anything-filelist.all.filelist  2.45s user 0.03s system 99% cpu 2.488 total
head -n 200 > /dev/null  0.00s user 0.00s system 0% cpu 2.485 total


次にCに変えた場合。

[mtym]% time LANG=C grep -ih anything /tmp/anything-filelist.all.filelist | head -n 200 > /dev/null
LANG=C grep -ih anything /tmp/anything-filelist.all.filelist  0.01s user 0.04s system 116% cpu 0.043 total
head -n 200 > /dev/null  0.01s user 0.00s system 30% cpu 0.032 total


予想以上に速くなる。というわけで anything-filelist で grep 実行時に LANG の値を変えたいのだけどやり方がよくわからないので advice を設定して、grep を実行するコマンドライン文字列に手を加えることにする。

(defadvice agp-command-line (after insert-LANG=C-into-agp-command-line)
  (while (string-match "\\(^\\|| \\)\\(grep -ih\\)" ad-return-value)
    (setq ad-return-value (replace-match "LANG=C \\2" t nil ad-return-value 2))))

(ad-activate 'agp-command-line)


これで結構速くなった。

それでもなんか遅かったりする

とくにマッチする行が 200 を越える場合に。


anything-grep-candidates-fast-directory-regexp を設定している場合、shell-command-to-string を使って grep が実行されるらしい。試しに shell-command-to-string で grep を実行してみると

(shell-command-to-string "grep -ih anything /tmp/anything-filelist.all.filelist | head -n 200")
"~/archives/emacs/anything/
~/archives/emacs/anything/descbinds-anything.el
~/archives/emacs/anything/anything-complete.el

grep: 出力の書込み: Broken pipe
grep: 出力の書込み: Broken pipe
grep: 出力の書込み: Broken pipe
"


こんな感じで 200 行以降は Broken pipe のエラーが延々と出力されている。これを抑制すればよさそうなのだけど、これまたやりかたが分からない。なのでまたまた advice で grepコマンドラインに手を加えて標準エラー出力を /dev/null にリダイレクトするようにしてみる。

(defadvice agp-command-line (after insert-Redirection-of-STDERR-into-agp-command-line)
  (when (string-match "| head -n" ad-return-value)
    (setq ad-return-value (replace-match "2>/dev/null \\&" t nil ad-return-value nil))))


これで 200 行を越える場合にも候補表示に時間がかかるといったことはなくなった。pattern を入力すると一瞬で候補が表示される感じ。


というわけで、LANG 値の変更とエラー出力のリダイレクトで anything-fielist をストレスなく使うことができるようになった。ただこのコマンドラインの変更を anything-filelist のときだけにしたいのだけど、今は grep-candidates を使う source 全てに適用されてしまうのがちょっと気になる。

popwin.el でポップアップ表示したバッファを再度ポップアップさせる

popwin.elを導入しみました。こういうの欲しかったけど自分じゃ作れない...。


で、一度ポップアップウィンドウで表示したバッファをもう一度、ポップアップウィンドウで表示させたい気がしたので慣れない elisp を書いてみた。


↓こんなの。F8 を押すと popwin:special-display-config に設定したポップアップ対象のバッファをポップアップで表示させます。もう一度 F8 を押すと別のポップアップ対象バッファに切り替わります。(elisp はほとんど書かないので変な書き方をしているだろうと思います)

(defvar my-popwin:seq-store-count 0)
(defvar my-popwin:buffer-list '())

(defun my-popwin:filter (pred lst)
  (if (null lst) '()
    (if (funcall pred (car lst))
        (cons (car lst) (my-popwin:filter pred (cdr lst)))
      (my-popwin:filter pred (cdr lst)))))

(defun my-popwin:repopup-target-p (buffer)
  (loop with name = (buffer-name buffer)
        with mode = (with-current-buffer buffer major-mode)
        for (pattern . keywords) in popwin:special-display-config
        thereis
        (destructuring-bind (&key regexp width height position noselect)
            keywords
          (cond
           ((and (stringp pattern)
                 regexp)
            (string-match pattern name))
           ((stringp pattern)
            (string= pattern name))
           ((symbolp pattern)
            (eq pattern mode))
           (t (error "Invalid pattern: %s" pattern))))))

(defun my-popwin:repopup-target-buffer-list ()
  (my-popwin:filter
   'my-popwin:repopup-target-p
   (buffer-list)))


(defun my-popwin:next-buffer ()
  (if (eq last-command this-command)
      (incf my-popwin:seq-store-count)
    (setq my-popwin:seq-store-count 0)
    (setq my-popwin:buffer-list (my-popwin:repopup-target-buffer-list)))
  (if (< 0 (length my-popwin:buffer-list))
      (nth (mod my-popwin:seq-store-count (length my-popwin:buffer-list))
           my-popwin:buffer-list)
    '()))

(defun my-popwin:repopup-window ()
  (interactive)
  (let ((buffer (my-popwin:next-buffer)))
    (unless (null buffer)
      (popwin:close-popup-window)
      (popwin:display-buffer buffer t))))

(global-set-key (kbd "<f8>") 'my-popwin:repopup-window)