zshからanything.elを利用してシームレスな履歴検索を

冬のはずなのにまだまだ薄着で過ごしています。というかセーターとかマフラーとか持ってないし。
道行く人達は冬の装いが多いですが、私は寒さに対してはやっぱり鈍感なようです。暖房代がかからないのは助かります(違

さて、つい先日、id:rubikitch さんが「anything.elを使ってzshの履歴検索をする - http://rubikitch.com/に移転しました」にて、anything.elをフル活用してzshの履歴検索を行う方法を提示されていました。
なんか参考として私のはてダのエントリーが貼られていますが、完成度とか速度は比べものになりませんです。はい。

しかし私個人としては、ターミナルにいるのに毎回Emacsのフレームが出てくるのもあれかなぁ、と思いまして、なんとかzshの中だけで完結できないものかなぁ、となんとかやってみました。

色々試してみたところ、anything.elの基盤を利用することで、かなり満足のいく動作をするようになりました。ので、ログ代わりに貼り付けておきます。elispの関数名とか見ればよくわかると思いますが、↑の記事をがっつり参考にしています。

ちなみに私のEmacs

(emacs-version) → "GNU Emacs 23.1.50.1 (i686-pc-linux-gnu, GTK+ Version 2.14.7)"

という感じです。

以下ソースと使い方です。

Emacs lisp

まずはelisp側の準備です。anything.elが前提なので、インストールしていない方は是非インストールしましょう

M-x install-elisp-from-emacswiki anything.el
M-x install-elisp-from-emacswiki anything-match-plugin.el
M-x install-elisp-from-emacswiki anything-complete.el
M-x install-elisp-from-emacswiki shell-history.el
↑これはanything-complete-shell-historyで使用しますので、インストールしておきます。

で、以下を.emacs.elとかに貼り付けます。色々作法とか間違っている気がしないでもないですが、気にしない。

;; zshからanythingを利用するための設定。
(require 'anything-complete)
(defvar azhzle/history-list nil)
(defvar azhzle/cache nil)

(defvar azhzle/tmp-file "/tmp/.azh-tmp-file")

(defvar azhzle/dicision-keys '("A" "S" "D" "F" "G" "H" "J" "K" "L"
                               "Q" "W" "E" "R" "T" "Y" "U" "I" "O" "P"))

(defun anything-zsh-history-from-zle-init ()
  (let ((anything-sources '(anything-c-source-complete-shell-history)))
    (anything-initialize)
    (unless azhzle/history-list
      (setq azhzle/history-list (azhzle/get-history-list)))
    (setq azhzle/cache azhzle/history-list))
  )

(defun azhzle/input (str)
  (interactive)
    (let* ((anything-pattern str)
           (lists (anything-compute-matches
                   `(,@anything-c-source-complete-shell-history
                     ,(cons 'candidates 'azhzle/cache)))))
      (with-temp-buffer
        (loop for i from 1 upto (if (<= (length lists) (length azhzle/dicision-keys))
                                    (length lists)
                                  (length azhzle/dicision-keys))
              for line in lists
              do (insert (nth (1- i) azhzle/dicision-keys) ":"
                         (substring-no-properties (car line)) "\n")
              do (message (car line)))
        (set-buffer-file-coding-system 'utf-8-unix t)
        (write-region (point-min) (point-max) azhzle/tmp-file))
      (setq azhzle/cache lists)
      ))

(defun azhzle/get-history-list ()
  (let ((lists '()))
    (with-current-buffer (shell-history-buffer)
      (loop for i from 1 upto (1- (count-lines (point-min) (point-max)))
            initially (goto-char (point-min))
            initially (setq zsh-p t)
            do (add-to-list 'lists
                            (list (acsh-get-line (line-beginning-position)
                                                 (line-end-position))))
            do (next-line)))
    lists
  ))

(defun azhzle/dicision-history (history-num)
  (interactive)
  (with-temp-buffer
    (erase-buffer)
    (insert (substring-no-properties (car (nth (- history-num 1)
                                               azhzle/cache))))
    (write-region (point-min) (point-max) azhzle/tmp-file)
  ))

(defun azhzle/reset-history ()
  (setq azhzle/history-list (azhzle/get-history-list))
  (setq azhzle/cache azhzle/history-list))

zshrc

そして上だけじゃなく、.zshrcに以下のコードを貼り付けてみてください。

## 各固定値
typeset -A HISTORY_DICISION_KEYS
set -A HISTORY_DICISION_KEYS A 1 S 2 D 3 F 4 G 5 H 6 J 7 K 8 L 9 Q 10 \
    W 11 E 12 R 13 T 14 Y 15 U 16 I 17 O 18 P 19 Z 20 X 21 C 22 V 23 B 24 N 25 M 26

ISHR_MENU_LENGTH=26
ISHR_FILENAME="/tmp/.azh-tmp-file"

function ishr-search-history-from-anything() {
    emulate -L zsh
    local key=$1

    # 特定キーが押されたら、該当する位置の履歴をバッファに表示する。
    if [[ -n "${HISTORY_DICISION_KEYS[$key]}" && -n "$BUFFER" ]]; then
        zle -A .self-insert self-insert

        emacsclient --eval "(azhzle/dicision-history ${HISTORY_DICISION_KEYS[$key]})"
        BUFFER=`cat $ISHR_FILENAME`
        zle -R -c

        zle accept-line
        return 1
    fi
    return 0
}

function ishr-update-status() {
    emacsclient --eval "(azhzle/input \"$BUFFER\")" &> /dev/null
    zle -M "`cat $ISHR_FILENAME`"
    zle -R
}

function ishr-self-insert() {
    emulate -L zsh
    LBUFFER+=${KEYS[-1]}
    ishr-search-history-from-anything ${KEYS[-1]}
    (( ! $? )) && ishr-update-status
}

function ishr-backward-delete-char() {
    emulate -L zsh
    zle .backward-delete-char
    ishr-update-status
}

function ishr-reset-history() {
    emacsclient --eval "(azhzle/reset-history)"
    ishr-update-status
}

zle -N incremental-search-history-menu
function incremental-search-history-menu() {
    # インクリメンタル履歴検索を行えるように準備等を行う。
    emulate -L zsh
    integer stat

    # 各種必要な変数の初期化。
    ishr-init-variables
    emacsclient --eval "(anything-zsh-history-from-zle-init)" &> /dev/null
    emacsclient --eval "(azhzle/input \"$BUFFER\")" &> /dev/null

    zle -N self-insert ishr-self-insert
    zle -N backward-delete-char ishr-backward-delete-char
    zle recursive-edit
    stat=$?
    zle -A .self-insert self-insert
    zle -A .backward-delete-char backward-delete-char

    rm -f $ISHR_FILENAME
    zle -R -c

    (( stat )) && zle send-break

    return $?
}

bindkey "^[@" incremental-search-history-menu

bindkey はお好みでどうぞ。本格的に利用するなら ^R とかでもよろしいかと思いますが、色々制限とかありますので気をつけてください。
履歴の選択は、各履歴の左端にA: 〜とかS: 〜 とか出ますので、対応した大文字を入力すれば履歴が登録されます。

で、実際の動作速度ですが、emacsclient の起動とか実行とかがやたらと早いため、最初(履歴をリストにします)以外は、zshオンリーでやっていたときとは比較にならないくらいサクサクです。
大体 400K 弱の履歴ファイルを作成して試してみましたが、それでもかなりサクサクいけるようです。

以下制限です。

  • .zsh_historyの変更に追随できない
    • limitを決めて末尾から毎回 acsh-get-line をすれば、.zsh_history自体は revert しているため、多分できます。

anything-compute-matches を利用しているため、スペース区切りで複数単語での検索とかもバッチリいけます。

でも多分環境依存とかは酷いような気がしますので(ぉぃ)、もし試された方で「動かんぞぉ!」という方がいらっしゃいましたら連絡下さい。