2022-05-11 18:40:58 +00:00
#+TITLE : Emacs Lisp Configuration
#+AUTHOR : Howard X. Abrams
#+DATE : 2022-05-11
A literate programming file for configuring Emacs for Lisp programming.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :exports none
2022-05-11 18:40:58 +00:00
;;; ha-lisp --- configuring Emacs for Lisp programming. -*- lexical-binding: t; -* -
;;
2023-02-23 17:35:36 +00:00
;; © 2022-2023 Howard X. Abrams
2022-06-18 00:25:47 +00:00
;; Licensed under a Creative Commons Attribution 4.0 International License.
2022-05-11 18:40:58 +00:00
;; See http://creativecommons.org/licenses/by/4.0/
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams >
;; Maintainer: Howard X. Abrams
;; Created: May 11, 2022
;;
;; This file is not part of GNU Emacs.
;;
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
;; /Users/howard.abrams/other/hamacs/ha-lisp.org
;; And tangle the file to recreate this one.
;;
;;; Code:
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
* Introduction
2022-06-18 00:25:47 +00:00
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.
2022-05-11 18:40:58 +00:00
New, /non-literal/ source code comes from [[file:templates/emacs-lisp-mode.el ][emacs-lisp-mode template ]]:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-05-11 18:40:58 +00:00
(ha-auto-insert-file (rx ".el" eol) "emacs-lisp-mode.el")
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
* Syntax Display
** Dim those Parenthesis
The [[https://github.com/tarsius/paren-face ][paren-face ]] project lowers the color level of parenthesis which I find better.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-05-11 18:40:58 +00:00
(use-package paren-face
:hook (emacs-lisp-mode . paren-face-mode))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
2022-06-18 00:25:47 +00:00
Show code examples with the [[https://github.com/xuchunyang/elisp-demos ][elisp-demos ]] package.
#+begin_src emacs-lisp
2022-05-11 19:39:14 +00:00
(use-package elisp-demos
:config
(advice-add 'describe-function-1 :after #'elisp-demos-advice-describe-function-1))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
* Navigation and Editing
2022-05-11 19:39:14 +00:00
** Goto Definitions
2022-05-12 16:40:35 +00:00
Wilfred’ s [[https://github.com/Wilfred/elisp-def ][elisp-def ]] project does a better job at jumping to the definition of a symbol at the point, so:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-05-11 19:39:14 +00:00
(use-package elisp-def
:hook (emacs-lisp-mode . elisp-def-mode))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 19:39:14 +00:00
This /should work/ with [[help:evil-goto-definition ][evil-goto-defintion ]], as that calls this list from [[help:evil-goto-definition-functions ][evil-goto-definition-functions ]]:
- [[help:evil-goto-definition-imenu ][evil-goto-definition-imenu ]]
- [[help:evil-goto-definition-semantic ][evil-goto-definition-semantic ]]
2022-06-18 00:25:47 +00:00
- [[help:evil-goto-definition-xref ][evil-goto-definition-xref ]] … to show what calls a function
2022-05-11 19:39:14 +00:00
- [[help:evil-goto-definition-search ][evil-goto-definition-search ]]
2022-06-18 00:25:47 +00:00
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 [[https://github.com/BurntSushi/ripgrep ][ripgrep ]] is pretty fast, I’ ll call it instead of attempting to build a [[https://stackoverflow.com/questions/41933837/understanding-the-ctags-file-format ][CTAGS ]] table. Oooh, the =rg= takes a =—json= option, which makes it easier to parse.
2022-05-12 16:40:35 +00:00
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-05-12 16:40:35 +00:00
(defun ha-org-code-block-jump (str pos)
"Go to a literate org file containing a symbol, STR.
The POS is ignored."
2022-05-23 05:06:26 +00:00
;; 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)))
2022-05-12 16:40:35 +00:00
(ignore-errors
(let* ((default-directory (projectile-project-root))
(command (format "rg --json '\\(def[^ ]+ %s ' *.org" str))
2022-05-13 16:47:31 +00:00
(results (thread-last command
2022-05-23 05:06:26 +00:00
shell-command-to-list
second
json-parse-string))
2022-05-13 16:47:31 +00:00
(file (thread-last results
2022-05-23 05:06:26 +00:00
(gethash "data")
(gethash "path")
(gethash "text")))
2022-05-13 16:47:31 +00:00
(line (thread-last results
2022-05-23 05:06:26 +00:00
(gethash "data")
(gethash "line_number"))))
2022-05-12 16:40:35 +00:00
(find-file file)
(goto-line line))))
2022-05-23 05:06:26 +00:00
(add-to-list 'evil-goto-definition-functions 'ha-org-code-block-jump)
2022-06-18 00:25:47 +00:00
#+end_src
2022-06-16 19:16:29 +00:00
And in case I need to call it directly:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-08-22 22:58:30 +00:00
(defun ha-goto-definition ()
(interactive)
(evil-inner-WORD))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
** Clever Parenthesis
We need to make sure we keep the [[https://github.com/Fuco1/smartparens ][smartparens ]] project always in /strict mode/ , because who wants to worry about paren-matching:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-05-11 18:40:58 +00:00
(use-package smartparens
:custom
(smartparens-global-strict-mode t)
2022-08-22 22:58:30 +00:00
:config
(sp-with-modes sp-lisp-modes
;; disable ', as it's the quote character:
(sp-local-pair "'" nil :actions nil))
(sp-with-modes (-difference sp-lisp-modes sp-clojure-modes)
;; use the pseudo-quote inside strings where it serve as hyperlink.
(sp-local-pair "`" "'"
:when '(sp-in-string-p
sp-in-comment-p)
:skip-match (lambda (ms _mb _me)
(cond
((equal ms "'") (not (sp-point-in-string-or-comment)))
(t (not (sp-point-in-string-or-comment)))))))
2022-05-11 18:40:58 +00:00
:hook
(prog-mode . smartparens-strict-mode))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
The [[https://github.com/luxbock/evil-cleverparens ][evil-cleverparens ]] solves having me create keybindings to the [[https://github.com/Fuco1/smartparens ][smartparens ]] project by updating the evil states with Lisp-specific bindings.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-05-11 18:40:58 +00:00
(use-package evil-cleverparens
:after smartparens
:custom
(evil-cleverparens-use-additional-bindings t)
(evil-cleverparens-use-additional-movement-keys t)
(evil-cleverparens-use-s-and-S t)
:init
(require 'evil-cleverparens-text-objects)
:hook
(prog-mode . evil-cleverparens-mode)) ;; All the languages!
;; Otherwise: (emacs-lisp-mode . evil-cleverparens-mode)
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
The /trick/ to being effective with the [[https://www.emacswiki.org/emacs/ParEdit ][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:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :tangle no
2022-05-11 18:40:58 +00:00
(+ 41 (* ‖1 3)) ⟹ (+ 41 (* ‖1) 3)
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
Use the ~>~ key to /slurp/ in outside objects into the current expression… in other words, move the paren away from the point. For instance:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :tangle no
2022-05-11 18:40:58 +00:00
(+ 41 (* ‖1) 3) ⟹ (+ 41 (* ‖1 3))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
*Opening Parens.* Those two keys seem straight-forward, but they behave differently when the are on the opening parens.
When the point (symbolized by ~‖~ ) is /on/ the opening paren, ~<~ moves the paren to the left. For instance:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :tangle no
2022-05-11 18:40:58 +00:00
(+ 41 ‖(* 1 3)) ⟹ (+ ‖(41 * 1 3))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
And the ~>~ moves the paren to the right. For instance:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :tangle no
2022-05-11 18:40:58 +00:00
(+ 41 ‖(* 1 3)) ⟹ (+ 41 * ‖(1 3))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
I would like to have a list of what keybindings that work in =normal= mode:
- ~M-h~ / ~M-l~ move back/forward by functions
- ~H~ / ~L~ move back/forward by s-expression
- ~M-i~ insert at the beginning of the form
- ~M-a~ appends at the end of the form
- ~M-o~ new form after the current sexp
- ~M-O~ new form /before/ the current sexp
- ~M-j~ / ~M-k~ drags /thing at point/ and back and forth in the form
- ~>~ slurp forward if at the end of form, at beginning, it barfs backwards
- ~<~ slurp backward if at start of form, at the end, it barfs forwards
- ~M-(~ / ~M-)~ wraps next/previous form in parens (braces and brackets work too)
- ~x~ unwraps if the point is on the =(= of an expression.
- ~D~ deletes an entire s-expression, but this can depend on the position of the point.
The other advantage is moving around by s-expressions. This takes a little getting used to, for instance:
- ~[~ and ~]~ move from paren to paren, essentially, from s-expression.
- ~H~ and ~L~ act similarly to the above.
- ~(~ and ~)~ move up to the parent s-expression
We need a real-world example. Let’ s suppose we entered this:
2023-03-17 16:55:12 +00:00
** Clever Keybindings
Adding a bunch of meta-key keybindings to the Normal state seems like I’ m going backwards away from the /key sequences/ of Evil. First, adding frequently used (especially key movements) on the ~g~ key seems nice. Since I never bother with [[help:find-file-at-point ][find-file-at-point ]], I figured I could re-purpose that keybinding:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :tangle no
2022-05-11 18:40:58 +00:00
(format "The sum of %d %d is %d" a b (+ a b))
2023-03-17 16:55:12 +00:00
(use-package evil-cleverparens
:general
(:states 'normal :keymaps 'prog-mode-map
"gf" '("evil cleverparens" . evil-cleverparens-hydra/body)
"H" 'evil-cp-backward-sexp
"L" 'ha-cp-forward-sexp))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
But we forgot to define the =a= and =b= variables. One approach, after Escaping into the normal state, is to hit ~(~ to just to the beginning of the s-expression, and then type, ~M-(~ to wrap the expression, and type ~i~ to go into insert mode:
2023-03-17 16:55:12 +00:00
For all the rest, why not make a Hydra using the pretty-hydra project:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :tangle no
2022-05-11 18:40:58 +00:00
(‖ (format "The sum of %d %d is %d" a b (+ a b)))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
And now we can enter the =let= expression.
2023-03-17 16:55:12 +00:00
(use-package pretty-hydra
:after evil-cleverparens
:config
(pretty-hydra-define evil-cleverparens-hydra
(:color red :quit-key "q")
("Movement"
(("f" ha-cp-beginning-of-next-defun "Next defun")
("C-f" evil-cp-end-of-defun nil) ; M-l
("F" evil-cp-beginning-of-defun "Prev defun") ; M-h
("j" evil-cp-forward-symbol-begin "Next symbol")
("k" evil-cp-backward-symbol-begin "Prev symbol")
("J" evil-cp-forward-symbol-end "Next symbol")
("K" evil-cp-backward-symbol-end "Prev symbol"))
"Move S-Exp"
(("h" evil-cp-backward-sexp "Prev s-exp") ; H
("C-l" evil-cp-forward-sexp nil) ; L
("l" ha-cp-forward-sexp "Next s-exp")
("C-u" evil-cp-backward-up-sexp nil)
("u" ha-sp-up-sexp "Up s-exp") ; See sp-up-sexp
("d" sp-down-sexp "Inside s-exp"))
"Slurping"
((">" evil-cp-> "Barf")
("<" evil-cp-< "Slurp")
("w" cp-wrap-round "Wrap")
("b" evil-cp-drag-backward "Drag Backward") ; M-k
("g" evil-cp-drag-forward "Drag forward")) ; M-j
"Manipulation"
(("=" sp-indent-defun "Indent defun") ; M-q
("J" sp-join-sexp "Join s-exp") ; M-j
("s" sp-splice-sexp "Splice s-exp") ; M-s
("S" sp-split-sexp "Split s-exp") ; M-S
("t" sp-transpose-sexp "Transpose s-exp") ; M-t
("T" sp-transpose-hybrid-sexp "Transpose hybrid")
("x" sp-convolute-sexp "Convolute s-exp") ; M-v
("r" sp-raise-sexp "Raise s-exp")) ; M-r
"Insert"
(("o" evil-cp-open-below-form "After" :color blue)
("O" evil-cp-open-above-form "Before" :color blue)
("a" ha-cp-append-end "append" :color blue)
("A" evil-cp-append "Append" :color blue)
("i" evil-cp-insert "Insert" :color blue))
"Other"
(("U" evil-undo "Undo")
("R" evil-redo "Redo")
("v" er/expand-region "Expand")
("V" er/contract-region "Contract")))))
2022-05-11 18:40:58 +00:00
Other nifty keybindings that I need to commit to muscle memory include:
| ~M-q~ | =sp-indent-defun= |
| ~M-J~ | =sp-join-sexp= |
| ~M-s~ | =sp-splice-sexp= |
| ~M-S~ | =sp-split-sexp= |
| ~M-t~ | =sp-transpose-sexp= |
| ~M-v~ | =sp-convolute-sexp= |
| ~M-r~ | =sp-raise-sexp= |
2023-03-17 16:55:12 +00:00
(defun ha-cp-beginning-of-next-defun (count)
"Move to the beginning of the next function."
(interactive "P")
(evil-cp-end-of-defun count)
(evil-cp-end-of-defun)
(evil-cp-beginning-of-defun))
(defun ha-sp-up-sexp (count)
"Better opposite of `sp-down-sexp'."
(interactive "P")
(evil-cp-backward-up-sexp count)
(evil-cp-backward-up-sexp)
(sp-down-sexp))
(defun ha-cp-forward-sexp (count)
"Better opposite of `evil-cp-backward-sexp'."
(interactive "P")
(evil-cp-forward-sexp count)
(evil-cp-forward-sexp)
(evil-cp-backward-sexp))
2022-05-11 18:40:58 +00:00
2023-03-17 16:55:12 +00:00
(defun ha-cp-append-end ()
"Append to the end of the current s-expression."
(interactive)
(when (looking-at (rx (any "{" "(" "[")))
(sp-down-sexp))
(sp-end-of-sexp)
(evil-cp-insert 1))
(defun ha-cp-append-after ()
"Append after the current s-expression."
(interactive)
(when (looking-at (rx (any "{" "(" "[")))
(sp-down-sexp))
(sp-end-of-sexp)
(evil-cp-append 1))
#+end_src
2022-05-11 18:40:58 +00:00
** Eval Current Expression
2022-05-13 21:35:26 +00:00
The [[https://github.com/xiongtx/eros ][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.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-05-13 21:35:26 +00:00
(use-package eros
:hook (emacs-lisp-mode . eros-mode))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-13 21:35:26 +00:00
2022-05-11 19:39:14 +00:00
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 [[help:evil-cp-next-closing ][evil-cp-next-closing ]] from cleverparens can help:
2022-05-11 18:40:58 +00:00
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-05-11 18:40:58 +00:00
(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
(evil-cp-next-closing)
(evil-cp-forward-sexp)
(call-interactively 'eval-last-sexp)))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
And we just need to bind it.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-05-11 18:40:58 +00:00
(ha-prog-leader
"e c" '("current" . ha-eval-current-expression))
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
* Technical Artifacts :noexport:
Let's =provide= a name so we can =require= this file:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :exports none
2022-05-11 18:40:58 +00:00
(provide 'ha-programming-elisp)
;;; ha-programming-elisp.el ends here
2022-06-18 00:25:47 +00:00
#+end_src
2022-05-11 18:40:58 +00:00
#+DESCRIPTION : configuring Emacs for Lisp programming.
#+PROPERTY : header-args:sh :tangle no
#+PROPERTY : header-args:emacs-lisp :tangle yes
#+PROPERTY : header-args :results none :eval no-export :comments no mkdirp yes
#+OPTIONS : num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
#+OPTIONS : skip:nil author:nil email:nil creator:nil timestamp:nil
#+INFOJS_OPT : view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js