Where parallels cross

Interesting bits of life

Repeat with me: Avy actions are awesome!

Too long; didn't read

Repeat the last Avy action to multiple places in the buffer.

The problem

Somebody on the Emacs Reddit channel shared a link to an amazing blog post by Karthik: https://karthinks.com/software/avy-can-do-anything/. That was greatly inspiring for me and made me finally move into a way of navigating and acting on text that is really rewarding for me.

Also what a combination that Karthik jumped onto the Emacs Buddy initiative! That gave me chance to discuss to him the problem that I am presenting to you now.

Given you read his post, you know that using Avy is not only about moving to a place in the visible buffer. Avy also lets you act on that.

Then my issue is simple: what about repeating the same action on many matches? Imagine I am in the Org Agenda buffer and I want to mark many entries done: the plan is making an Avy command for that and repeating it on matches until I am satisfied.

Since you can create a command for anything, you could have one to open links or even reply the same Slack message to many recipients.

How difficult would that be?

And there is a solution

Well if you get Karthik as a buddy, that is pretty straightforward! He helped me understand where to modify things.

Let me show you the result:

This is how Avy avy-goto-char-timer works:

(defun avy-goto-char-timer (&optional arg)
  "Read one or many consecutive chars and jump to the first one.
The window scope is determined by `avy-all-windows' (ARG negates it)."
  (interactive "P")
  (let ((avy-all-windows (if arg
                             (not avy-all-windows)
                           avy-all-windows)))
    (avy-with avy-goto-char-timer
      (setq avy--old-cands (avy--read-candidates))
      (avy-process avy--old-cands))))

Basically (avy--read-candidates) asks you the input and selects your candidates overlaying letters on them. (avy-process avy--old-cands) instead applies the action on one of them.

After reading Karthik's advice I thought I could manage by simply running (avy-process avy--old-cands) again in my action, and that worked badly. The highlights would be broken after my first match.

So, after another email exchange I was given a plan: refactor Avy in smaller functions to reuse the bits I needed for my feature.

And I (mostly) followed it. This is what I got to:

(defun my/avy--read-candidates ()
  (let ((re-builder #'regexp-quote)
        break overlays regex)
    (unwind-protect
        (progn
          (avy--make-backgrounds
           (avy-window-list))
          ;; Unhighlight
          (dolist (ov overlays)
            (delete-overlay ov))
          (setq overlays nil)
          ;; Highlight
          (when (>= (length avy-text) 1)
            (let ((case-fold-search
                   (or avy-case-fold-search (string= avy-text (downcase avy-text))))
                  found)
              (avy-dowindows current-prefix-arg
                (dolist (pair (avy--find-visible-regions
                               (window-start)
                               (window-end (selected-window) t)))
                  (save-excursion
                    (goto-char (car pair))
                    (setq regex (funcall re-builder avy-text))
                    (while (re-search-forward regex (cdr pair) t)
                      (unless (not (avy--visible-p (1- (point))))
                        (let* ((idx (if (= (length (match-data)) 4) 1 0))
                               (ov (make-overlay
                                    (match-beginning idx) (match-end idx))))
                          (setq found t)
                          (push ov overlays)
                          (overlay-put
                           ov 'window (selected-window))
                          (overlay-put
                           ov 'face 'avy-goto-char-timer-face)))))))
              ;; No matches at all, so there's surely a typo in the input.
              (unless found (beep))))
          (nreverse (mapcar (lambda (ov)
                              (cons (cons (overlay-start ov)
                                          (overlay-end ov))
                                    (overlay-get ov 'window)))
                            overlays)))
      (dolist (ov overlays)
        (delete-overlay ov))
      (avy--done))))

(defun my/avy-repeat-action ()
  (setq avy--old-cands (my/avy--read-candidates))
  (avy-process avy--old-cands))

I extracted only the bits I needed of avy--read-candidates into my/avy--read-candidates. The driving idea is that since I am repeating an action, I don't want to ask the user again for input, but reuse the old one. So I removed that part of avy--read-candidates. The rest looks like the original Avy function.

So how do we use this? Define your Avy action like this:

(defun avy-action-org-agenda-done (pt)
    (save-excursion
      (goto-char pt)
      (org-agenda-todo))
    (select-window
     (cdr (ring-ref avy-ring 0)))
    (my/avy-repeat-action)
    t)

(setf (alist-get ?D avy-dispatch-alist) 'avy-action-org-agenda-done)

This uses (org-agenda-todo) to change the state of your Org Agenda entry. So if you match over TODO and press the D key and start following Avy's hints you can keep marking tasks done until you need. When you are finished just press C-g.

Conclusion

Now you can repeat Avy commands without a sweat! I hope this repetition will save me tons of (little) time :)

Happy repeating!

Comments