zshでanything.el風履歴検索(その3)

久々に電車で「今時の若者」を見かけました。四人で八人分の席を占拠、自分の部屋のような格好、周囲を鑑みないやかましさ等々。
ああいうのを見ると、「今時の若者は・・・」と言われてもしゃあないなぁ、と思いました。でも注意する勇気なんてないチキンな私。

まーでも、普通の会社とかにもし行くとしたら、いずれそんな行動がどれだけ意味の無いものだったかを知るんでしょう。そういえば今年の新人とかは、なんか人に興味を持つことができない人が多い傾向が強くなっているとか。
家族での食事時に、団欒が無いせいだとか。私の実家だとやかましいくらいなんですが。親子四人でクイズ番組でマジになっていたり。楽しい両親です。

そんなことはさておき、zshで頑張って履歴検索をしてみようシリーズ(?)の第三弾です。

以前id:rubikitch さんより挙げて頂いた改善案が↓ですが、前回はこのうち、2.だけを実装したようなものでした。
しかしやっぱりというかなんというか、こちらとしてはリアルタイムに検索をしたいのです。別にzshでやらんでもemacs+anythingでやれるやんけ、とかそういうのは野暮ってものです。八割位は興味と意地ではありますけど。

しかし、キーを押すたびに反応してしまうため重すぎるのが唯一の問題である。重さを解消する方法としては以下が考えられる。

・ キーを押して0.3秒間応答がなければ検索処理を開始する(anything方式)
・ 前回の検索処理の結果から絞り込む(QuickSilver方式)
・ .zsh_historyを高速に履歴検索する専用プログラムを作成し、パイプとかで通信する(コマンド起動がオーバーヘッドの場合)

さて、第三弾まで来たところで、やっぱり1.にチャレンジしたいと思います。ですが、前回まで利用していた recursive-editでは、非同期の実行とかはできません。
また、色々試したところ、

「zleの内部とその外部では環境変数の影響範囲が異なる」

っぽく(大いなる勘違いの可能性あり)、また、zleの内部以外から、zle -Rとかzle -Mとかを呼ぶことが出来ませんでした。これは当然と言えば当然ではありますが。

なんとかならんかと色々man zshallとかやってみたところ、「zsh/sched」というものを発見しました。
zshでスケジュール実行でき、かつ zleの最中で入力待ちをさせることができるという特徴があり、「これっきゃない!」ということに。

具体的な利用方法は最後のソースを見て頂くとして、第二弾からの変更点は下のようになっています。

  • キーを押してから一定時間(0.3秒)入力が無い場合、検索を開始する
    • 検索時、長い場合があるので、検索開始時点でメッセージと検索中のクエリを表示するようにした
  • 前回検索したクエリは、再度検索した場合はすでに前回の検索結果を利用する。
    • zshを起動してから終了するまで、基本的にはそのまま残り続ける。
  • 検索履歴をリセットできるようにした。(bindkey -eをしている場合、C-uでリセット)
  • 検索結果を、A〜Mまでの大文字で選択できるようにした。
  • 検索結果を利用しない場合、リターンキーで終了(入力した文字はそのまま)できるようにした。

えー、ここまで書いておいてなんなんですが、まだちょっと不安定です・・・。キーを入力して明らかに0.3秒とか経過していないのに検索したり、逆に検索しなかったりと・・・。
また、zshスクリプトにも関わらずなんか汚ないです。その辺はご容赦ください。
それと、linux上のzshでは概ね快適なんですが、cygwin上のzshでは非常に使いづらいです。(職場のwindow2000+cygwin環境で確認済)ですので、cygwin上では使わない方がいいかもです。

では以下が実際のソースです。割り当てるキーバインドは御自由にどうぞ。また、改善案とか追加案があればどうぞ教えて頂ければと思います。
ていうか長すぎですね。すみません。

# --- anything風に履歴を検索するためのメニュー。

## 必要なモジュール
zmodload zsh/sched

## 各固定値
typeset -A HISTORY_INCREMENTAL_KEYS
set -A HISTORY_INCREMENTAL_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_BASEFILE=/tmp/ishr.tmp
ISHR_MENU_LENGTH=26

## キー待ち関係
ISHR_KEY_WAIT_SECOND=0.05
ISHR_KEY_INTERVAL=300

### 内部使用値
ISHR_PREVIOUS_QUERY=""
ISHR_TIMER_COUNT=()
ISHR_END_SEARCH=0
ISHR_SEARCH_QUERY=""
typeset -A ISHR_PREVIOUS_RESULTS

ishr-init-variables() {
    # 各固有変数のリセット
    ISHR_PREVIOUS_QUERY=""
    ISHR_SEARCH_QUERY=""
    ISHR_TIMER_COUNT=(`date "+%s %N"`)
    ISHR_TIMER_COUNT[2]=`echo $ISHR_TIMER_COUNT[2] | cut -b 1-3`
    ISHR_END_SEARCH=0
}

function ishr-interrupt-keys() {
    # 前回キーが押された時からどれだけ時間が経過しているかを調査する。
    # 調査する対象は、ISHR_TIMER_COUNT[2] - 秒 * 1000 + ISHR_TIMER_COUNT[3] - ナノ秒 | cut -b 1-3
    # として、ミリ秒単位として検査する。
    sleep $ISHR_KEY_WAIT_SECOND

    local tmp q=$ISHR_SEARCH_QUERY
    local diff d
    tmp=($ISHR_TIMER_COUNT)
    d=(`date "+%s %N"`)
    d[2]=`echo ${d[2]} | cut -b 1-3`

    ((diff=(${d[1]}-${tmp[1]})*1000+${d[2]}-${tmp[2]}))

    # この時点で、diffはミリ秒単位である。
    # diffの値が、事前定義されている間隔以上である場合、検索を行う。

    # 前回キーを押した時点から指定の時間だけ経過している場合、検索を行う。
    if [ -n "$q" -a "$ISHR_PREVIOUS_QUERY" != "$q" -a $diff -ge $ISHR_KEY_INTERVAL ]; then
        ishr-search-history-regexp $q
    fi
    
    if [ $ISHR_END_SEARCH -eq 1 ]; then
        sched +0 ishr-interrupt-keys
    else
        ISHR_END_SEARCH=2
    fi
}

function ishr-search-history-regexp() {
    local query=$1
    local target_file=""

    (( $+ISHR_PREVIOUS_RESULTS[(e)$query] )) && target_file=$ISHR_PREVIOUS_RESULTS[$query]

    zle -M "`echo \"please wait...now searching : $query\"`"
    zle -R

    if [ ! -f "$target_file" -o -z "$target_file" ]; then
        # クエリに対するファイルを割り振る。
        ISHR_PREVIOUS_RESULTS+=($query ${ISHR_BASEFILE}.`ls ${ISHR_BASEFILE}* | wc -l`)

        if [ -n "$ISHR_PREVIOUS_QUERY" -a `echo "$query" | grep -c "$ISHR_PREVIOUS_QUERY"` -gt 0 ]; then
            # 前回候補が今回のqueryに含まれていたら、前回の結果から絞り込む。
            egrep -i "$query" $ISHR_PREVIOUS_RESULTS[$ISHR_PREVIOUS_QUERY] | uniq > $ISHR_PREVIOUS_RESULTS[$query]
        else
            # 前回候補と異なる場合、または初回検索の場合は、初回取得した全履歴から検索する。
            egrep -i "$query" $ISHR_BASEFILE | uniq  > $ISHR_PREVIOUS_RESULTS[$query]
        fi
        target_file=${ISHR_PREVIOUS_RESULTS[$query]}
    fi

    ISHR_PREVIOUS_QUERY=$query

    zle -M  "`ruby -e 'puts \"query : \" + ARGV[1] ; %w[A S D F G H J K L Q W E R T Y U I O P Z X C V B N M].zip(open(ARGV[0]).readlines){|k,l| print %[#{k}: #{l}]}' =(tail -$ISHR_MENU_LENGTH $target_file | tac) $query`"
    zle -R

}

function dicision-history-regexp() {
    local key=$1

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

        local fname=$ISHR_PREVIOUS_RESULTS[${(e)ISHR_PREVIOUS_QUERY}]
        BUFFER="`tac $fname | head -${HISTORY_INCREMENTAL_KEYS[$key]} | tail -1 | perl -pe 's/\\\\n/\\021\\n/g'`"
        zle -R

        ishr-accept-line
    fi
}


#########################################################
# incremental-search-history-menu内部で利用する内部関数 #
#########################################################

function _ishr-update-status() {
    ISHR_SEARCH_QUERY=$BUFFER
    ISHR_TIMER_COUNT=(`date "+%s %N"`)
    ISHR_TIMER_COUNT[2]=`echo ${ISHR_TIMER_COUNT[2]} | cut -b 1-3`
    if [[ $ISHR_END_SEARCH = 0 ]]; then
        ISHR_END_SEARCH=1
        sched +0 ishr-interrupt-keys
    fi
}

function _ishr-stop-interrupt() {
    ISHR_END_SEARCH=0
    sched -1 2&> /dev/null

    ishr-init-variables
}

#########################################################

function ishr-self-insert() {

    LBUFFER+=${KEYS[-1]}
    dicision-history-regexp ${KEYS[-1]}
    _ishr-update-status
}

function ishr-backward-delete-char() {
    zle -A .backward-delete-char backward-delete-char
    zle backward-delete-char
    zle -N backward-delete-char ishr-backward-delete-char

    # 一文字削除して、検索クエリを再度設定する。
    _ishr-update-status
}


function ishr-accept-line() {

    _ishr-stop-interrupt
    zle -A .accept-line accept-line
    zle accept-line
}

function ishr-reset-results() {
    # 結果を保存しているものをリセットする。
    _ishr-stop-interrupt

    typeset -A ISHR_PREVIOUS_RESULTS
    ISHR_PREVIOUS_RESULTS=()

    rm ${ISHR_BASEFILE}*

    history -n 1 | uniq > $ISHR_BASEFILE
}

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

    # 初回限定で、履歴全体をuniq化して保持しておく。
    history -n 1 | uniq > $ISHR_BASEFILE

    # 各種必要な変数の初期化。
    ishr-init-variables

    # 正規表現によるインクリメンタル検索を行う。

    zle -N kill-whole-line ishr-reset-results
    zle -N self-insert ishr-self-insert
    zle -N backward-delete-char ishr-backward-delete-char
    zle -N accept-line ishr-accept-line
    zle recursive-edit
    stat=$?
    zle -A .kill-whole-line kill-whole-line
    zle -A .self-insert self-insert
    zle -A .backward-delete-char backward-delete-char
    zle -A .accept-line accept-line

    # 各種終了処理
    _ishr-stop-interrupt

    zle -R -c

    (( stat )) && zle send-break

    return $?
}

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

# ---