Blackthorn

joined 1 year ago
 

Hey, I was working on this one off and on for a few days after briefly trying out skeleton-mode, yasnippet, and some other stuff, and not really being too happy with them. I find that I have a lot of repetitive editing tasks where I need to do something to a small block of code a lot, but in the process change some names or values in a way that's just a little bit different each time. Normally this is where people would start to reach for yasnippet and auto-yasnippet, which is fine if that works for them, but personally that's just a bit more heavyweight and powerful than what I normally need. What I wanted was just a way to enhance a regular Emacs keyboard macro to support that sort of thing, so I wrote this. If it helps you too, wonderful!

To use, just press C-x Q (that's a capital Q, not a lowercase q) during keyboard macro recording, and press your normal enter/return/minibuffer-exit when you're done. I went through a lot of trouble figuring out how to make the minibuffer exit also exit the sub-macro recording!

;; Keyboard macro enhancement. If you call this, instead of
;; kbd-macro-query, it will prompt the user for a value. This value
;; will then be inserted into the buffer. Every time you call the
;; macro, you can provide a different value.
;;
;; Alternatively, you can call this with a prefix argument. If you do
;; this, you will be prompted for a symbol name. Instead of the value
;; being inserted into the buffer, it will be saved in the symbol
;; variable. You can then manipulate it or do whatever you want with
;; that symbol as part of the keyboard macro. Just, when you do this,
;; make sure you don't use minibuffer history at all when defining the
;; macro, or you can get some unexpected behavior if you save your
;; macro for later use and try it a few hours later!
(defun config:macro-query (symbol)
  (interactive
   (list (when current-prefix-arg
           (intern (read-from-minibuffer "symbol: ")))))
  (cl-flet ((internal-exit ()
              (interactive)
              (exit-recursive-edit)))
    (let ((making-macro defining-kbd-macro)  ;; Save value.
          (temp-map (make-sparse-keymap)))
      ;; Temporarily bind what is normally C-M-c (exit-recursive-edit)
      ;; to RET, so RET will work in the spawned minibuffer.
      (set-keymap-parent temp-map minibuffer-local-map)
      (substitute-key-definition 'exit-minibuffer #'internal-exit temp-map)
      (let ((exit-fn (set-transient-map temp-map (-const t))))
        (cl-flet ((also-quit-minibuffer ()
                    ;; When this is called (advice after
                    ;; recursive-edit), this-command should be
                    ;; whatever was just used to exit the recursive
                    ;; edit / minibuffer. Usually RET. Push that onto
                    ;; the unread commands, and it will immediately
                    ;; get picked up and executed. We also want to use
                    ;; this moment to turn off the transient map.
                    (funcall exit-fn)
                    (when making-macro
                      (setq unread-command-events
                            (nconc (listify-key-sequence (this-command-keys))
                                   unread-command-events)))))
          (advice-add 'recursive-edit :after #'also-quit-minibuffer)
          (unwind-protect
              (let ((input (minibuffer-with-setup-hook
                               (lambda ()
                                 (kbd-macro-query t))
                             (read-from-minibuffer "Value: "))))
                (if symbol
                    (set symbol input)
                  (insert input)))
            ;; Ensure that the advice and minibuffer map goes back to
            ;; normal.
            (advice-remove 'recursive-edit #'also-quit-minibuffer)
            (funcall exit-fn)))))))
(global-set-key (kbd "C-x Q") 'config:macro-query)