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))
Better Function Help
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:
- evil-goto-definition-imenu
- evil-goto-definition-semantic
- evil-goto-definition-xref … to show what calls a function
- evil-goto-definition-search
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 toM-Delete
text-objects
- Add more text-objects, I wrote my for s-expressions, but I might try these
atom-movement
- The
e
/w
andb
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, andM-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, andV
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")))))