hamacs/ha-programming-elisp.org
2023-03-17 09:55:12 -07:00

337 lines
15 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#+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 <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. Im 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
Wilfreds [[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 dont have to learn anything, Im 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, Ill 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. Lets suppose we entered this:
** Clever Keybindings
Adding a bunch of meta-key keybindings to the Normal state seems like Im 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