#+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 ** Better Function Help Let’s take advantage of [[https://github.com/Wilfred/helpful][helpful]] package for getting more information into the =describe-function= call. #+begin_src emacs-lisp (use-package helpful) #+end_src And we should extend it with the [[https://github.com/xuchunyang/elisp-demos][elisp-demos]] project: #+begin_src emacs-lisp (use-package elisp-demos :after helpful :config (ha-local-leader :keymaps '(emacs-lisp-mode-map lisp-mode-map) "d a" '("add helpful demo" . elisp-demos-add-demo)) (advice-add 'helpful-update :after #'elisp-demos-advice-helpful-update)) #+end_src Find a function without a good demonstration? Call =elisp-demos-add-demo=. * 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 ** Lispy I like the idea of [[https://github.com/abo-abo/lispy][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 [[help:lispy-ace-paren][lispy-ace-paren]] that puts an /ace label/ on every parenthesis, allowing me to quickly jump to any s-expression. #+begin_src emacs-lisp (use-package lispy :config (evil-define-key 'normal emacs-lisp-mode-map ;; Use C-SPC to mark single letter, as these seem to be nicer expansions: "M-v" 'lispy-mark ; Mark entire s-expression "g f" 'lispy-ace-paren "g F" 'lispy-ace-symbol) (evil-define-key 'visual emacs-lisp-mode-map "o" 'lispy-mark-symbol) ; Mark symbol since "o" doesn't do anything (ha-local-leader :keymaps '(emacs-lisp-mode-map lisp-mode-map) "r" '(:ignore t :which-key "refactor") "r i" '("cond→if" . lispy-to-ifs) "r c" '("if→cond" . lispy-to-cond) "r d" '("λ→𝑓" . lispy-to-defun) "r l" '("𝑓→λ" . lispy-to-lambda) "r f" '("flatten" . lispy-flatten) "r b" '("bind var" . lispy-bind-variable) "r u" '("unbind var" . lispy-unbind-variable) "e d" '("edebug" . lispy-edebug) "e j" '("debug-step-in" . lispy-debug-step-in) "e R" '("eval-and-replace" . lispy-eval-and-replace) "d d" '("describe" . lispy-describe) "t t" '("ert" . lispy-ert))) #+end_src ** Lispyville I want an Evil version of [[Lispy]]. The [[https://github.com/noctuid/lispyville][lispyville project]] builds on it to make it Evil. From the README: #+begin_quote 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). #+end_quote Many of the operations supplied by =lispyville= don’t require learning anything new. Similar to [[Clever Parenthesis]], we can For instance, if our point is placed at this location in this code: #+begin_src emacs-lisp :tangle no (message "The answer is %d" (+ 2 (* 8 5)‸ 9 (+ 1 4))) #+end_src Pressing ~D~ results in: #+begin_src emacs-lisp :tangle no (message "The answer is %d" (+ 2 (* 8 5)‸)) #+end_src And doesn’t delete the trailing parenthesis. 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 *Note:* I used to use the [[https://github.com/luxbock/evil-cleverparens][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. #+begin_src emacs-lisp (use-package lispyville :hook ((emacs-lisp-mode lisp-mode) . lispyville-mode)) #+end_src Now we need to define additional key movements: #+begin_src emacs-lisp (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)) #+end_src 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 [[file:ha-config.org::*Better Parenthesis with Text Object][own version]] 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 [[file:ha-config.org::*Evil Surround][Evil Surround]] 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 [[file:ha-config.org::*Expand Region][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 ** 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 (unless (looking-at (rx (any ")" "]"))) (lispyville-next-closing)) (call-interactively 'eval-last-sexp))) #+end_src And we just need to bind it. #+begin_src emacs-lisp (ha-local-leader :keymaps '(emacs-lisp-mode-map lisp-mode-map) "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