988c0dac2e
Oh, and let's fix the FILETAGS. Thank goodness for woccurrrrrr.
361 lines
18 KiB
Org Mode
361 lines
18 KiB
Org Mode
#+title: Emacs Lisp Configuration
|
||
#+author: Howard X. Abrams
|
||
#+date: 2022-05-11
|
||
#+tags: emacs programming lisp
|
||
|
||
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 <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:
|
||
#+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=.
|
||
|
||
Wilfred’s [[https://github.com/Wilfred/suggest.el][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.
|
||
#+begin_src emacs-lisp
|
||
(use-package suggest
|
||
:config
|
||
(ha-local-leader :keymaps '(emacs-lisp-mode-map lisp-mode-map)
|
||
"H" '("suggestions" . suggest)))
|
||
#+end_src
|
||
* Navigation
|
||
** 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)))
|
||
;; 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))
|
||
#+end_src
|
||
* Editing
|
||
** 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
|
||
(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
|
||
|
||
(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)
|
||
"r >" '("to thread last" . lispy-toggle-thread-last)
|
||
|
||
"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
|
||
(when (fboundp 'evil-define-key)
|
||
(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
|
||
(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)))
|
||
#+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
|
||
** Refactoring
|
||
Wilfred’s [[https://github.com/Wilfred/emacs-refactor/tree/master#elisp][emacs-refactor]] package can be helpful if you turn on =context-menu-mode= and …
|
||
#+begin_src emacs-lisp
|
||
(use-package emacs-refactor
|
||
:general
|
||
(:states '(normal visual) :keymaps 'emacs-lisp-mode-map
|
||
;; Often know what functions are available:
|
||
", r r" '("refactor menu" . emr-show-refactor-menu)
|
||
;; These are my favorites ...
|
||
|
||
;; Extracts the current s-expression or region to function:
|
||
", r F" '("to function" . emr-el-extract-function)
|
||
", r V" '("to variable" . emr-el-extract-variable)
|
||
;; Converts the current let to a let*
|
||
", r *" '("toggle let*" . emr-el-toggle-let*)
|
||
;; asks for a variable, and extracts the code in a region
|
||
;; or the current s-expression, into the nearest let binding
|
||
", r l" '("to let" . emr-el-extract-to-let)))
|
||
#+end_src
|
||
|
||
The idea of stealing some of Clojure Mode’s refactoring is brilliant (see [[https://isamert.net/2023/08/14/elisp-editing-development-tips.html#clojure-thread-lastfirst-all-from-https-github-com-clojure-emacs-clojure-mode-clojure-mode][the original idea]]), however, I’m already using Lispy’s =toggle-thread-last=.
|
||
#+begin_src emacs-lisp :tangle no
|
||
(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)))
|
||
#+end_src
|
||
* Evaluation
|
||
** 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 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.
|
||
|
||
#+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
|
||
(if (region-active-p)
|
||
(eval-region (region-beginning) (region-end))
|
||
|
||
(unless (looking-at (rx (any ")" "]")))
|
||
(sp-end-of-sexp))
|
||
|
||
(if (fboundp 'eros-eval-last-sexp)
|
||
(call-interactively 'eros-eval-last-sexp)
|
||
(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 e" '("current" . ha-eval-current-expression))
|
||
#+end_src
|
||
** Debugging
|
||
The =edebug= debugger is built into Emacs, so all I need is an easier way to instrument a function:
|
||
#+begin_src emacs-lisp
|
||
(ha-local-leader :keymaps '(emacs-lisp-mode-map lisp-mode-map)
|
||
"e D" '("set edebug" . (lambda ()
|
||
(interactive)
|
||
(setq current-prefix-arg '(4)) ; C-u
|
||
(call-interactively 'eval-defun))))
|
||
#+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:t todo:nil tasks:nil tags:nil date:nil
|
||
#+options: skip:nil author:nil email:nil creator:nil timestamp:nil
|
||
#+infojs_opt: view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
|