hamacs/ha-programming-elisp.org
2023-04-01 16:32:16 -07:00

298 lines
14 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
Perhaps we should have an Emacs Lisp-specific leader for advanced commands:
#+begin_src emacs-lisp
(general-create-definer ha-elisp-leader
:states '(normal visual motion)
:keymaps '(emacs-lisp-mode-map lisp-mode-map)
:prefix ","
:global-prefix "<f17>"
:non-normal-prefix "S-SPC")
#+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
Lets 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-elisp-leader
"h 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
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
** 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
"gf" 'lispy-ace-paren
"gF" 'lispy-ace-symbol)
(ha-elisp-leader
"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)
"h 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 lispys “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= dont 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
(message "The answer is %d" (+ 2 (* 8 5) 9 (+ 1 4)))
#+end_src
Pressing ~D~ results in:
#+begin_src emacs-lisp
(message "The answer is %d" (+ 2 (* 8 5)))
#+end_src
And doesnt 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 dont really use these types of code manipulation in my day-job programming languages of Python and YAML.
#+begin_src emacs-lisp
(use-package lispyville
:after lispy
:hook ((emacs-lisp-mode-hook lisp-mode-hook) . lispyville-mode)
: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)
;; 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 doesnt 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 isnt 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-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