hamacs/ha-programming-elisp.org
Howard Abrams db2d27b66c Goto definitions written in org files
Got a hacky approach to jumping to function definitions that are
written in an literate org file using ripgrep. Hopefully, this will
work well.
2022-05-12 09:40:35 -07:00

9.3 KiB
Raw Blame History

Emacs Lisp Configuration

A literate programming file for configuring Emacs for Lisp programming.

Introduction

While I program in a lot of languages, I seem to be writing all my helper tools and scripts in … Emacs Lisp. So Im cranking this up to 11.

New, non-literal source code comes from emacs-lisp-mode template:

  (ha-auto-insert-file (rx ".el" eol) "emacs-lisp-mode.el")

Syntax Display

Dim those Parenthesis

The paren-face project lowers the color level of parenthesis which I find better.

  (use-package paren-face
    :hook (emacs-lisp-mode . paren-face-mode))

Show code examples with the elisp-demos package. This is really helpful.

  (use-package elisp-demos
    :config
    (advice-add 'describe-function-1 :after #'elisp-demos-advice-describe-function-1))

Navigation and Editing

Goto Definitions

Wilfreds elisp-def project does a better job at jumping to the definition of a symbol at the point, so:

  (use-package elisp-def
    :hook (emacs-lisp-mode . elisp-def-mode))

This should work with evil-goto-defintion, as that calls this list from evil-goto-definition-functions:

I love packages that add functionality but I dont have to learn anything. However, 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 where it is defined in the org file. Since ripgrep is pretty fast, Ill call it instead of attempting to build a CTAGS table. Oooh, the rg takes a —json option, which makes it easier to parse.

  (defun ha-org-code-block-jump (str pos)
    "Go to a literate org file containing a symbol, STR.
  The POS is ignored."
    (ignore-errors
      (let* ((default-directory (projectile-project-root))
             (command (format "rg --json '\\(def[^ ]+ %s ' *.org" str))
             (results (->> command
                           shell-command-to-list
                           second
                           json-parse-string))
             (file    (->> results
                           (gethash "data")
                           (gethash "path")
                           (gethash "text")))
             (line    (->> results
                           (gethash "data")
                           (gethash "line_number"))))
        (find-file file)
        (goto-line line))))

  (add-to-list 'evil-goto-definition-functions ' ha-org-code-block-jump)

Clever Parenthesis

We need to make sure we keep the smartparens project always in strict mode, because who wants to worry about paren-matching:

  (use-package smartparens
    :custom
    (smartparens-global-strict-mode t)
    :hook
    (prog-mode . smartparens-strict-mode))

The evil-cleverparens solves having me create keybindings to the smartparens project by updating the evil states with Lisp-specific bindings.

  (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)

The trick to being effective with the 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:

  (+ 41 (* 1 3))    (+ 41 (* 1) 3)

Use the > key to slurp in outside objects into the current expression… in other words, move the paren away from the point. For instance:

  (+ 41 (* 1) 3)    (+ 41 (* 1 3))

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:

  (+ 41 (* 1 3))    (+ (41 * 1 3))

And the > moves the paren to the right. For instance:

  (+ 41 (* 1 3))   (+ 41 * (1 3))

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:

(format "The sum of %d %d is %d" a b (+ a b))

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:

  ( (format "The sum of %d %d is %d" a b (+ a b)))

And now we can enter the let expression.

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

Eval Current Expression

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 evil-cp-next-closing from cleverparens can help:

(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)))

And we just need to bind it.

  (ha-prog-leader
    "e c" '("current" . ha-eval-current-expression))