Endless Parentheses

Ramblings on productivity and technical subjects.

profile for Malabarba on Stack Exchange

Commands to thread and unwind code in Emacs-Lisp

As you may remember, one of the commands I like the most from the clj-refactor package are the ones that thread and unwind Clojure code forms for you. Now that Emacs is also getting built-in threading macros, I figured the best way to give them a fair chance in life is to also make them pretty convenient to use.

The point here is that you can place point before a paren, invoke a command, and a regular code form gets transformed into a threading macro, or vice-versa. See the linked post for what that means. Instead of writing whole new commands for that, I had a fun time just hacking clj-refactor commands to work on thread-first/last.

It should go without saying, you need clj-refactor installed for this to work.

(define-key emacs-lisp-mode-map "\C-ctf"
  #'endless/elisp-thread-first)
(define-key emacs-lisp-mode-map "\C-ctl"
  #'endless/elisp-thread-last)
(define-key emacs-lisp-mode-map "\C-ctu"
  #'endless/elisp-unwind)
(define-key emacs-lisp-mode-map "\C-cta"
  #'endless/elisp-unwind-all)

(defun endless/elisp-thread-last ()
  "Turn the form at point into a `thread-last' form."
  (interactive)
  (cljr-thread-last-all nil)
  (save-excursion
    (when (search-backward "->>" nil 'noerror)
      (replace-match "thread-last"))))

(defun endless/elisp-thread-first ()
  "Turn the form at point into a `thread-first' form."
  (interactive)
  (cljr-thread-first-all nil)
  (save-excursion
    (when (search-backward "->" nil 'noerror)
      (replace-match "thread-first"))))

(defun endless/elisp-unwind ()
  "Unwind thread at point or above point by one level.
Return nil if there are no more levels to unwind."
  (interactive)
  (let ((p (point)))
    ;; Find a thread above.
    (when (save-excursion
            (forward-sexp 1)
            (and (search-backward-regexp "\\_<thread-\\(first\\|last\\)\\_>" nil 'noerror)
                 ;; Ensure that it contains the original point.
                 (save-match-data (forward-char -1)
                                  (forward-sexp 1)
                                  (> (point) p))))
      (replace-match (if (string= (match-string 1) "first")
                         "->" "->>"))
      (let ((thread-beginnig (match-beginning 0)))
        (prog1 (cljr-unwind)
          (save-excursion
            (goto-char thread-beginnig)
            (when (looking-at "\\_<->>?\\_>")
              (replace-match (if (string= (match-string 0) "->")
                                 "thread-first" "thread-last")))))))))

(defun endless/elisp-unwind-all ()
  "Fully unwind thread at point or above point."
  (interactive)
  (while (endless/elisp-unwind)))

Maybe just writing it from scratch would have made for shorter code (it would certainly be more robust). But such is life. I try not to dwell too much on quick hacks.

comments powered by Disqus