Emacs Lisp Configuration

Table of Contents

A literate programming file for configuring Emacs for Lisp programming.

Introduction

While I program in a lot of languages, I seem to be writing all my helper tools and scripts in … Emacs Lisp. I’m cranking this up to 11.

New, non-literal source code comes from emacs-lisp-mode template:

(ha-auto-insert-file (rx ".el" eol) "emacs-lisp-mode.el")

Syntax Display

Dim those Parenthesis

The paren-face project lowers the color level of parenthesis which I find better.

(use-package paren-face
  :hook (emacs-lisp-mode . paren-face-mode))

Show code examples with the elisp-demos package.

(use-package elisp-demos
  :config
  (advice-add 'describe-function-1 :after #'elisp-demos-advice-describe-function-1))

Helpful Functions

Let’s take advantage of helpful package for getting more information into the describe-function call.

(use-package helpful)

And we should extend it with the elisp-demos project:

(use-package elisp-demos
  :after helpful
  :config
  (advice-add 'helpful-update :after #'elisp-demos-advice-helpful-update))

Find a function without a good demonstration? Call elisp-demos-add-demo.

Wilfred’s suggest function helps you find the right function. Basically, you type in the parameters of a function, and then the desired output, and it will write the function call.

(use-package suggest)

Navigation

Goto Definitions

Wilfred’s elisp-def project does a better job at jumping to the definition of a symbol at the point, so:

(use-package elisp-def
  :hook (emacs-lisp-mode . elisp-def-mode))

This should work with evil-goto-defintion, as that calls this list from evil-goto-definition-functions:

While I love packages that add functionality and I don’t have to learn anything, I’m running into an issue where I do a lot of my Emacs Lisp programming in org files, and would like to jump to the function definition defined in the org file. Since ripgrep is pretty fast, I’ll call it instead of attempting to build a CTAGS table. Oooh, the rg takes a —json option, which makes it easier to parse.

(defun ha-org-code-block-jump (str pos)
  "Go to a literate org file containing a symbol, STR.
The POS is ignored."
  ;; Sometimes I wrap a function name in `=' characters, and these should be removed:
  (when (string-match (rx "=" (group (one-or-more any)) "=") str)
    (setq str (match-string 1 str)))
  ;; In an org-file, a function may pick up the initial #'
  (when (string-match (rx (optional "#") (optional "'" ) (group (one-or-more any))) str)
    (setq str (match-string 1 str)))
  (ignore-errors
    (let* ((default-directory (project-root (project-current)))
           (command (format "rg --json '\\(def[^ ]+ %s ' *.org" str))
           (results (thread-last command
                                 shell-command-to-list
                                 second
                                 json-parse-string))
           (file    (thread-last results
                                 (gethash "data")
                                 (gethash "path")
                                 (gethash "text")))
           (line    (thread-last results
                                 (gethash "data")
                                 (gethash "line_number"))))
      (find-file file)
      (goto-line line))))

(if (boundp 'evil-goto-definition-functions)
    (add-to-list 'evil-goto-definition-functions 'ha-org-code-block-jump)
  (add-to-list 'xref-backend-functions 'ha-org-code-block-jump))

Editing

Lispy

I like the idea of lispy for making a Lisp-specific keybinding state (similar to Evil).

My primary use-case is for its refactoring and other unique features. For instance, I love lispy-ace-paren that puts an ace label on every parenthesis, allowing me to quickly jump to any s-expression.

(use-package lispy
  :config
  (when (fboundp 'evil-define-key)
    (evil-define-key '(normal visual) lispyville-mode-map
      ;; Jump to interesting places:
      "gf" '("ace paren"  . lispy-ace-paren)
      "gF" '("ace symbol" . lispy-ace-symbol)
      (kbd "M-v") '("mark s-exp" . lispy-mark)))   ; Mark entire s-expression

  (pretty-hydra-define lispy-debug nil
    ("Debug"
     (("d" lispy-edebug "Start")
      ("j" lispy-debug-step-in "Jump in")
      ("r" lispy-eval-and-replace "Eval/Replace"))
     "Instrument"
     (("f" (eval-defun t) "Function"))
     ))

  (pretty-hydra-define lisp-refactor nil
    ("To"
     (("i" lispy-to-ifs "cond→if")
      ("c" lispy-to-cond "if→cond")
      ("t" lispy-toggle-thread-last "to thread")
      ("d" lispy-to-defun "λ→𝑓")
      ("l" lispy-to-lambda "𝑓→λ"))
     "Convert"
     (("F" lispy-flatten "flatten")
      ("b" lispy-bind-variable "bind var")
      ("B" lispy-unbind-variable "unbind var")))))

Lispyville

I want an Evil version of Lispy. The lispyville project builds on it to make it Evil. From the README:

The main difference from an evil state is that lispy’s “special” is contextually based on the point (special is when the point is before an opening delimiter, after a closing delimiter, or when there is an active region).

Many of the operations supplied by lispyville don’t require learning anything new. Similar to , we can For instance, if our point is placed at this location in this code:

(message "The answer is %d" (+ 2 (* 8 5)‸ 9 (+ 1 4)))

Pressing D results in:

(message "The answer is %d" (+ 2 (* 8 5)))

And doesn’t delete the trailing parenthesis.

The trick to being effective with the paredit-family of extensions is learning the keys. The killer “app” is the slurp/barf sequence. Use the < key, in normal mode, to barf (or jettison)… in other words, move the paren closer to the point. For instance:

(+ 41 (* ‸1 3))(+ 41 (* ‸1) 3)

Use the > key to slurp in outside objects into the current expression… in other words, move the paren away from the point. For instance:

(+ 41 (* ‸1) 3)(+ 41 (* ‸1 3))

Note: I used to use the evil-cleverparens project to have similar keybindings but in all programming languages. I found that lispyville is a little more reliable, and that I don’t really use these types of code manipulation in my day-job programming languages of Python and YAML.

(when (fboundp 'evil-define-key)
  (use-package lispyville
    :hook ((emacs-lisp-mode lisp-mode) . lispyville-mode)))

Now we need to define additional key movements:

(when (fboundp 'evil-define-key)
  (use-package lispyville
    :config
    (lispyville-set-key-theme '(operators atom-movement
                                          commentary slurp/barf-lispy additional-wrap
                                          additional additional-insert))

    (evil-define-key '(normal insert emacs) lispyville-mode-map
      (kbd "M-h") 'lispyville-beginning-of-defun
      (kbd "M-l") 'lispyville-beginning-of-next-defun
      (kbd "M-i") 'lispyville-insert-at-beginning-of-list ; These are useful
      (kbd "M-a") 'lispyville-insert-at-end-of-list ; and I want to use
      (kbd "M-o") 'lispyville-open-below-list ; these in insert
      (kbd "M-O") 'lispyville-open-above-list ; or Emacs state.

      ;; The c-w theme is VI-specific. I still use Emacs' M-Delete:
      (kbd "M-DEL")  'lispyville-delete-backward-word)

    ;; Sentence and paragraph movement doesn't make sense in a Lisp world,
    ;; so I redefine these based on my own personal expectations:
    (evil-define-key 'normal lispyville-mode-map
      "H" 'lispyville-backward-sexp-begin
      (kbd "M-H") 'lispyville-backward-sexp-end
      "L" 'lispyville-forward-sexp-begin
      (kbd "M-L") 'lispyville-forward-sexp-end
      "(" 'lispyville-previous-opening
      ")" 'lispyville-next-closing
      "{" 'lispyville-backward-up-list
      "}" 'lispyville-next-opening

      "[ f" 'lispyville-beginning-of-defun
      "] f" 'lispyville-beginning-of-next-defun
      "] F" 'lispyville-end-of-next-defun)

    ;; Visually high-light a region, just hit `(' to wrap it in parens.
    ;; Without smartparens, we need to insert a pair of delimiters:
    (evil-define-key '(visual insert emacs) lispyville-mode-map "(" 'lispy-parens)
    (evil-define-key '(visual insert emacs) lispyville-mode-map "[" 'lispy-brackets)
    (evil-define-key '(visual insert emacs) lispyville-mode-map "{" 'lispy-braces)))

Instead of converting all keybindings, the project supplies key themes to grab specific keybinding groups.

operators
basic VI operators that keep stuff balanced
c-w
replaces the C-w, but since that is VI-specific, I rebind this to M-Delete
text-objects
Add more text-objects, I wrote my for s-expressions, but I might try these
atom-movement
The e / w and b keys will move by symbols instead of words.
additional-movement
Adds new movement keys, H / L for s-expr and the ( / ) for getting to closest expressions. This doesn’t work well, but is easy to re-implement.
commentary
Replace gc for un/commenting Lisp elements.
slurp/bar-lispy
always allow < / > to slurp/barf even inside an s-expression.
additional
New M- bindings for manipulating s-expressions. M-J is very cool.
additional-insert
M-i insert at beginning, and M-a to insert at the end of a list.
wrap
like but with one less keystroke. M-( M-( wraps the entire line.
additional-wrap
is another version of the wrap that automatically wraps current symbol, and then you can slurp in the rest.
mark
The v will highlight current symbol, and V will highlight current s-expression. Continues to work with Expand Region.

New bindings to remember:

>
slurp
<
barf
H
backward s-expression
L
forward s-expression
M-h
beginning of defun
M-l
end of defun
M-i
insert at beginning of list
M-a
insert at end of list
M-o
open below list … never worry about inserting into a bunch of closing parens.
M-O
open above list
M-j
drag forward
M-k
drag backward
M-J
join
M-s
splice … I could use specific examples for these operations so I would know when to use them.
M-S
split
M-r
raise s-expression
M-R
raise list
M-t
transpose s-expressions
M-v
convolute s-expression

These are all good, but the primary keys I need to figure out, are the s-expression movement keys:

{
backward up list … nice to hit once (maybe twice), but isn’t something to use to navigate
}
next opening parenthesis
(
previous opening paren
)
next closing parenthesis

Refactoring

Wilfred’s emacs-refactor package can be helpful if you turn on context-menu-mode and …

(use-package emr
  ;; :straight (:host github :repo "Wilfred/emacs-refactor")
  :config
  (pretty-hydra-define+ lisp-refactor nil
    ("To 𝛌"
     (;; Often know what functions are available:
      ("a" emr-show-refactor-menu "all")
      ;; Extracts the current s-expression or region to function:
      ("f" emr-el-extract-function "to function")
      ("v" emr-el-extract-variable "to variable")
      ;; Converts the current let to a let*
      ("*" emr-el-toggle-let* "toggle let*")
      ;; asks for a variable, and extracts the code in a region
      ;; or the current s-expression, into the nearest let binding
      ("L" emr-el-extract-to-let "to let")))))

The idea of stealing some of Clojure Mode’s refactoring is brilliant (see the original idea), however, I’m already using Lispy’s toggle-thread-last.

(use-package clojure-mode
  :general
  (:states '(normal visual) :keymaps 'emacs-lisp-mode-map
           ", r >" '("to thread last" . clojure-thread-last-all)
           ", r <" '("to thread first" . clojure-first-last-all)))

Evaluation

Eval Current Expression with eros

The eros package stands for Evaluation Result OverlayS for Emacs Lisp, and basically shows what each s-expression is near the cursor position instead of in the mini-buffer at the bottom of the window.

(use-package eros
  :hook (emacs-lisp-mode . eros-mode))

A feature I enjoyed from Spacemacs is the ability to evaluate the s-expression currently containing the point. Not sure how they made it, but Lispyville has a lispyville-next-closing function to jump to that closing paren (allowing a call to eval-last-sexp), and if I save the position using save-excursion, I get this feature.

(defun ha-eval-current-expression ()
  "Evaluates the expression the point is currently 'in'.
It does this, by jumping to the end of the current
expression (using evil-cleverparens), and evaluating what it
finds at that point."
  (interactive)
  (save-excursion
    (if (region-active-p)
        (eval-region (region-beginning) (region-end))

      (sp-end-of-sexp)
      (if (fboundp 'eros-eval-last-sexp)
          (call-interactively 'eros-eval-last-sexp)
        (call-interactively 'eval-last-sexp)))))

Major Mode Hydra

All the above loveliness can be easily accessible with a major-mode-hydra defined for emacs-lisp-mode:

(use-package major-mode-hydra
  :config
  (major-mode-hydra-define emacs-lisp-mode nil
    ("Evaluating"
     (("e" ha-eval-current-expression "Current")
      ("d" lispy-debug/body "Debugging")
      ("f" eval-defun "Function")
      ("b" eval-buffer "Buffer"))
     "Editing"
     (("r" lisp-refactor/body "Refactoring"))
     "Documentation"
     (("a" elisp-demos-add-demo "Add Demo")
      ("H" suggest "Suggestions")))))