#+TITLE: Emacs Lisp Configuration #+AUTHOR: Howard X. Abrams #+DATE: 2022-05-11 A literate programming file for configuring Emacs for Lisp programming. #+begin_src emacs-lisp :exports none ;;; ha-lisp --- configuring Emacs for Lisp programming. -*- lexical-binding: t; -*- ;; ;; © 2022-2023 Howard X. Abrams ;; Licensed under a Creative Commons Attribution 4.0 International License. ;; See http://creativecommons.org/licenses/by/4.0/ ;; ;; Author: Howard X. Abrams ;; 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: #+end_src * 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 [[file:templates/emacs-lisp-mode.el][emacs-lisp-mode template]]: #+begin_src emacs-lisp (ha-auto-insert-file (rx ".el" eol) "emacs-lisp-mode.el") #+end_src * 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. #+begin_src emacs-lisp (use-package paren-face :hook (emacs-lisp-mode . paren-face-mode)) #+end_src Show code examples with the [[https://github.com/xuchunyang/elisp-demos][elisp-demos]] package. #+begin_src emacs-lisp (use-package elisp-demos :config (advice-add 'describe-function-1 :after #'elisp-demos-advice-describe-function-1)) #+end_src * Navigation and Editing ** Goto Definitions 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: #+begin_src emacs-lisp (use-package elisp-def :hook (emacs-lisp-mode . elisp-def-mode)) #+end_src 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]] - [[help:evil-goto-definition-xref][evil-goto-definition-xref]] … to show what calls a function - [[help:evil-goto-definition-search][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 [[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. #+begin_src emacs-lisp (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))) (ignore-errors (let* ((default-directory (projectile-project-root)) (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)))) (add-to-list 'evil-goto-definition-functions 'ha-org-code-block-jump) #+end_src And in case I need to call it directly: #+begin_src emacs-lisp (defun ha-goto-definition () (interactive) (evil-inner-WORD)) #+end_src ** 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: #+begin_src emacs-lisp (use-package smartparens :custom (smartparens-global-strict-mode t) :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))))))) :hook (prog-mode . smartparens-strict-mode)) #+end_src 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. #+begin_src emacs-lisp (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) #+end_src 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: #+begin_src emacs-lisp :tangle no (+ 41 (* ‖1 3)) ⟹ (+ 41 (* ‖1) 3) #+end_src Use the ~>~ key to /slurp/ in outside objects into the current expression… in other words, move the paren away from the point. For instance: #+begin_src emacs-lisp :tangle no (+ 41 (* ‖1) 3) ⟹ (+ 41 (* ‖1 3)) #+end_src *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: #+begin_src emacs-lisp :tangle no (+ 41 ‖(* 1 3)) ⟹ (+ ‖(41 * 1 3)) #+end_src And the ~>~ moves the paren to the right. For instance: #+begin_src emacs-lisp :tangle no (+ 41 ‖(* 1 3)) ⟹ (+ 41 * ‖(1 3)) #+end_src 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: ** 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: #+begin_src emacs-lisp :tangle no (format "The sum of %d %d is %d" a b (+ a b)) (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)) #+end_src 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: For all the rest, why not make a Hydra using the pretty-hydra project: #+begin_src emacs-lisp :tangle no (‖ (format "The sum of %d %d is %d" a b (+ a b))) #+end_src And now we can enter the =let= expression. (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"))))) 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= | (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)) (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 ** Eval Current Expression 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. #+begin_src emacs-lisp (use-package eros :hook (emacs-lisp-mode . eros-mode)) #+end_src 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: #+begin_src emacs-lisp (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))) #+end_src And we just need to bind it. #+begin_src emacs-lisp (ha-prog-leader "e c" '("current" . ha-eval-current-expression)) #+end_src * Technical Artifacts :noexport: Let's =provide= a name so we can =require= this file: #+begin_src emacs-lisp :exports none (provide 'ha-programming-elisp) ;;; ha-programming-elisp.el ends here #+end_src #+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