Nicolas Martyanoff — Brain dump About

Making IELM More Comfortable

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!

Share the word!

Liked my article? Follow me on Twitter or on Mastodon to see what I'm up to.