IELM is the Emacs Lisp REPL. It lets you evaluate any Elisp expression, prints
the output and keeps track of previous commands. While it is a serious step-up
from the humble eval-expression
, it can be uncomfortable on several aspects.
Let us improve it.
Adding Eldoc hints
Eldoc is a generic module used to display realtime documentation hints while you type. For Emacs Lisp, it means displaying docstrings when your cursor is over a symbol or when you start writing a function call.
When calling a function, you often need a quick remainder about the arguments.
Having Eldoc means you do not have to call C-h f
and deal with the
documentation buffer, and can just glance at the echo area.
To enable Eldoc in IELM, let us add it to the initialization hook:
(add-hook 'ielm-mode-hook 'eldoc-mode)
Using Paredit
Paredit is an essential minor mode that lets you manipulate Lisp code as expressions and not as simple text. If you are not using it and think that editing all these parentheses is annoying, you are missing out. Give it a try, you will not be disappointed.
To use Paredit in IELM, we add it to the initialization hook:
(add-hook 'ielm-mode-hook 'paredit-mode)
Infortunately there is a key conflict between Paredit and IELM. Paredit
overrides return
to execute paredit-RET
, meaning that the original
ielm-return
is not called.
The simplest way to fix it is to alter the Paredit keymap:
(define-key paredit-mode-map (kbd "RET") nil)
(define-key paredit-mode-map (kbd "C-j") 'paredit-newline)
We remove the entry associated with return
and use C-j
to insert a newline
character, useful to write a multiline expression.
Making the command history persistent
The most annoying part of IELM is that it does not have persistent history. I expect any interactive command system to store past entries since they can always be useful again. ZSH has persistence, so does the SLIME Common Lisp REPL. IELM does not, even though Comint (the mode IELM derives from) can handle it.
Let us add it. First we will write a function to configure Comint and load any entry stored in the history file if it exists:
(defun g-ielm-init-history ()
(let ((path (expand-file-name "ielm/history" user-emacs-directory)))
(make-directory (file-name-directory path) t)
(setq-local comint-input-ring-file-name path))
(setq-local comint-input-ring-size 10000)
(setq-local comint-input-ignoredups t)
(comint-read-input-ring))
We store the history in the ielm/history
file in the user Emacs directory,
mimicking the eshell/history
file used by Eshell.
Nothing complicated: we increase the number of entries stored in the history
file, and tell Comint to drop duplicate entries. We also are careful and use
setq-local
to make sure Comint settings are only modified for the current
buffer and not globally, since other buffers using different Comint-based
modes could have different requirements.
We want to call this function when IELM start:
(add-hook 'ielm-mode-hook 'g-ielm-init-history)
Reading the history is one thing. We need a way to add all expressions we
evaluate to it. It would have been nice to have a hook called everytime an
expression is entered, but we have to do without it. We are going to use Elisp
advices
to evaluate code each time a specific function is called. We want to write
the history file when ielm-send-input
returns:
(defun g-ielm-write-history (&rest _args)
(with-file-modes #o600
(comint-write-input-ring)))
(advice-add 'ielm-send-input :after 'g-ielm-write-history)
Advising functions are called with the same arguments as the advised function.
We do not care about them, so we use the &rest
keyword to match all
arguments passed to g-ielm-write-history
.
We are careful to use with-file-modes
to make sure the file is always
created as only readable by the user, since it may contain private
information.
This was not easy, but persistent history is worth it!
Useful key bindings
Finally we will bind two key combinations.
First the very common C-l
to clear the buffer. All modes deriving from
Comint have C-c M-o
, but it is awkward to type and C-l
is very common in
various applications.
(define-key inferior-emacs-lisp-mode-map (kbd "C-l")
'comint-clear-buffer)
Then we want a way to search through old expressions. Comint has
comint-history-isearch-backward-regexp
which is incredibly primitive. We can
get a far better experience with Helm.
The helm-comint-input-ring
function lets us browse among old expressions and
select one through incremental search. We bind it to C-r
:
(define-key inferior-emacs-lisp-mode-map (kbd "C-r")
'helm-comint-input-ring)
IELM is so much more comfortable to use now!