ielmのhistoryをcounselで検索できるようにする

incremental searchは絶対的に正義だ。これは揺るがない。

elispを最近結構書いているのでielmの使い勝手がslime/sbcl並に良くなってほしいが如何せん使ってる人が少ないのか全然hack記事がないので折角なので調べてみた。


ielmについて取り組む前にslimeのhistoryを検索できるelispを書いた。 コードはものすごく単純だ。

(defun my/slime-history ()
  (interactive)
  (insert
   (completing-read
    "choice history: "
    (with-temp-buffer
      (insert-file-contents "~/.slime-history.eld")
      (let ((read-circle t))
        (nreverse (read (current-buffer))))))))

この記事を書いてる時に気がついたが、 f.elを使えばもっとエレガントに書ける。

(defun my/slime-history ()
  (interactive)
  (insert
   (completing-read
    "choice history: "
    (-distinct (read (f-read-text "~/.slime-history.eld"))))))

ついでにkeymapを設定するならこんな感じだろうか。

(define-key slime-repl-mode-map (kbd "C-c C-r") 'my/slime-history))

これで快適に検索ができる。


さて、ielmでも同じことができるだろうか。今欲しいのはielmのhistory fileだ。 標準では出力してくれないのでどうやったら出来るのか調査が必要だ。

まずはみんな大好きWikiEmacsを読んだり、世の中に転がってる記事を読んだ。が、駄目。情報が殆ど載っていない。

しょうがない、 emacs-mirror/emacsielm.el のコードを読む。実はやってることはすごく単純だということがわかる。 define-derived-modecomint-mode から派生、ielm-map でkeymapを設定、あとはreplとしての機能を提供しているだけだ 。行数もせいぜい700行未満でほとんど処理がない。

結局の所、 emacs-mirror/emacscomint.el の機能を読む必要がある。 M-pM-n でhistoryを遡れるということはどこかで持っているはずだ、という推測を立てて調べていくとhistory fileの出力も出来ることがわかる。

以下のようなelispを書いた。history fileを指定して、起動時に読み込む。ielmのprocessがkillされた時にhookしてhistory fileに書き込むような処理を書いた。

(defun my/ielm-write-history-on-exit (process event)
  (comint-write-input-ring)
  (let ((buf (process-buffer process)))
    (when (buffer-live-p buf)
      (with-current-buffer buf
        (insert (format "\nProcess %s %s" process event))))))

(defun my/ielm-turn-on-history ()
  (let ((process (get-buffer-process (current-buffer))))
    (when process
      (setq comint-input-ring-file-name (concat (file-remote-p default-directory) "~/.ielm-history"))
      (setq comint-input-ring-size 100000)
      (setq comint-input-ignoredups t)
      (comint-read-input-ring)
      (add-hook 'kill-buffer-hook #'comint-write-input-ring)
      (define-key ielm-map (kbd "C-c C-r") 'my/ielm-history)
      (set-process-sentinel process #'my/ielm-write-history-on-exit))))

(add-hook 'ielm-mode-hook 'my/ielm-turn-on-history)

これで ~/.ielm-history にsexpが出力されるようになった。

process kill時にhistory fileに書き出すように処理を書いたので直近のhistoryは comint-input-ring で取得して読み込ませるようにした。

(defun my/ielm-history ()
  (interactive)
  (insert
   (completing-read
    "choice history: "
    (progn
      (let ((history nil))
        (dotimes (index (ring-length comint-input-ring))
          (push (ring-ref comint-input-ring index) history))
        history)))))

思ったよりもielm関連の記事がなくて辛かったがなんとかなった。 comint-mode は神。