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

今週は寒いようで、一気に気温が一桁になりましたね。でもコートは着ない私・・・すみません嘘です、コート持ってないんです。
防寒着繋がりですが、道を歩いているとよくマフラーをしている人を見かけますね。私は雪国出身ではありますが、生まれてこのかた数回しかマフラーしたことありません。
何かが首に触れているのがすんごい嫌なためですが、それでもちゃんとした服装をしていれば寒くないものです。多分。

さて、以前zshから無理矢理anything.elの機能を利用して履歴検索を高速にすることに成功しましたが、あれには何個か問題がありました。

  • 日本語が混じるような履歴を扱うと止まる
  • 毎回履歴全件から検索するため、履歴が多い環境だと遅延が発生する
  • なんかコードがイケてない

最後はとっても個人的なものですのでどうでもいいですが、特に最初のが致命的です。滅多に履歴に日本語が入ってくることは無いと思いますが、たまさか入ってくると、その時点でEmacs側の処理が停止するため、非常にストレスフルな環境に早変りしてしまいます。

で、上二つを合わせて、以下のように修正してみました。

  • 日本語が入っていても停止しないように
    • ですが日本語はとっても化けます。これは.zsh_historyがバイナリで保存されているような感じのためなんですが、どうすれば解決できるのがよくわかりません。
  • 毎回履歴を全件検索するが、本当に全件ではなく見付けた順に取得するように変更
    • 処理がちょいとダサいですが。
  • ちゃんとzshでzleを起動した時点で、バッファの文字列で検索が行われて結果を表示するように
    • 最初からやっておくべきでした、はい。

とりあえず以下がelispと.zshrcのソースです。必要なものは変わりません。

Emacs lisp
(require 'anything-complete)
(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* ((lists (azhzle/get-history-line str)))
      (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"))
        (set-buffer-file-coding-system 'raw-text-unix t)
        (write-region (point-min) (point-max) azhzle/tmp-file))
      (setq azhzle/cache lists)
      ))

(defun azhzle/get-history-line (pattern)
  (with-current-buffer (shell-history-buffer)
    (let ((lists '())
          (limit (length azhzle/dicision-keys))
          (matches '())
          (exit nil)
          (anything-pattern pattern)
          (zsh-p t))

      (loop while (or (not (bobp)) exit)
            initially (goto-char (point-max))
            if (or (bobp) exit (< limit (length matches)))
            do (return)
            else do
            (loop for i from 1 upto limit
                  if (or (bobp) (= (point-at-bol) (point-at-eol)))
                  do (previous-line) (return)
                  else do
                  (push (list (acsh-get-line (line-beginning-position)
                                             (line-end-position))) lists)
                  (previous-line))
            (setq matches (append matches
                                  (anything-compute-matches
                                   `(,@anything-c-source-complete-shell-history
                                     ,(cons 'candidates 'lists)
                                     (volatile)))))
            (setq lists nil))
      matches)))

(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)
  ))

## 各固定値
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
}

#######################################
# incremental-search-history-menu本体 #
#######################################

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

    # 各種必要な変数の初期化。
    emacsclient --eval "(anything-zsh-history-from-zle-init)" &> /dev/null
    ishr-update-status

    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は好みでどうぞ。
個人的には、これでなんとか常用できそうです。一番不安なのはこんなanything.elの使いかたしていーんだろうかということ。まぁいいか(ぁ