Avy can do anything

You’re using Avy wrong.

Too harsh? Let me rephrase: you’re barely using Avy. Still too broad? Okay, the noninflammatory version: Avy, the Emacs package to jump around the screen, lends itself to efficient composable usage that’s obscured by default.

Without burying the lede any further, here’s a demo that uses a single Avy command (avy-goto-char-timer) to do various things in multiple buffers and windows, all without manually moving the cursor:

Copy text, kill lines or regions, move text around, mark text, bring up help buffers, look up definitions, search google, check my spelling, the list goes on. I emphasize this again: Avy defines dozens of jump commands, but I’m only using one. This post breaks this down in detail so you can create your own version of this, but more importantly tries to explain why this is a neat idea.

This is the first of two parts in a series on Avy, an Emacs package for jumping around efficiently with the keyboard. Part 1 is about about supercharging built-in customization to do anything with Avy, or some approximation thereof. Part 2 will be a more technical (elisp-y) dive into writing more complex features for your individual needs. If you are interested in the short elisp snippets in this document, they are collated into a single file here.

FilterSelectAct

We see the same pattern repeated in most interactions with Emacs whose primary purpose isn’t typing text. To perform a task, we Filter, Select and Act in sequence:

Filter: Winnow a large pile of candidates to a smaller number, usually by typing in some text. These candidates can be anything, see below for examples.

Select: Specify a candidate as the one you need, usually with visual confirmation. If you filter a list down to one candidate, it’s automatically selected.

Act: Run the task with this candidate as an argument.

  • Want to open a file? Invoke a command, then type in text to filter a list of completions, select a completion, find-file.
  • Switch buffers? Type to filter, select a buffer, switch.
  • Autocomplete a symbol in code? Type a few characters to narrow a pop-up list, choose a candidate, confirm.

As ever, this model is a simplification. Helm, Ivy, Dired & co let you select multiple candidates to act on, for instance. We will put this qualification aside while we explore this idea.

This observation leads to several interesting ideas. For one, the FilterSelectAct process is often thought of as a single operation. Decoupling the filtering, selection and action phases (in code and in our heads) frees us to consider a flock of possibilities. Here’s minibuffer interaction:

The possibilities are, respectively: different completion styles (matching by regexps, flex, orderless), different selection interfaces (Icomplete, Ivy, Helm, Vertico, Selectrum and more by the day) and different action dispatchers (Embark, Helm/Ivy actions)1. A similar mini-Cambrian explosion has happened in the space of shell utilities since fzf arrived on the scene. In Emacs the minibuffer is where we’re used to selecting things, so it’s natural to think of these examples.

But this usage pattern extends to more than just minibuffer interaction. We often do things in regular buffers that involve the FilterSelectAct pattern. In fact, you can look at most interactions with Emacs (whose focus isn’t typing text) this way: when you call a goto-definition command with point–the text cursor–in some text, the filtering stage is skipped, the selection is made for you by a function from the thing-at-pt library, and a preset action is called. How about a mouse interaction? scroll through a window full of icons, move the mouse cursor to an icon and click on it. These completely redundant examples prime us for the more interesting cases. Consider Isearch:

When you type (after C-s), you automatically filter buffer text and select the nearest match. You can select each subsequent match in turn, or jump to the first or last match. The Act here is the process of moving the cursor to the match location, but it can be one of a few things, like running occur or query-replace on the search string. Many Isearch commands simultaneously filter, select and act, so we’re fitting a square peg in a round hole here2.

If you’ve spent any time using Isearch, you can appreciate the tradeoff involved in dividing a task into these three independently configurable phases. Lumping two or all three into a single operation makes Isearch a wicked fast keyboard interaction. When I use Isearch my brain is usually trying to catch up to my fingers. On the other hand, the advantage of modularity is expressive power. The three phase process is slower on the whole, but we can do a whole lot more by plugging in different pieces into each of the Filter , Select and Act slots. To see this, you have only to consider how many disparate tasks the minibuffer handles, and in how many different ways!

But back to Isearch: what can we do to decouple the three stages here? Not much without modifying its guts. It’s all elisp, so that’s not a tall order. For example, Protesilaos Stavrou adds many intuitive actions (such as marking candidates) to Isearch in this video. But it turns out we don’t need to modify Isearch, because Avy exists, has a killer Filter feature, and it separates the three stages like a champ. This makes for some very intriguing possibilities.

Wait, what’s Avy?

Avy is authored by the prolific abo-abo (Oleh Krehel), who also wrote Ivy, Counsel, Ace-Window, Hydra, Swiper and many other mainstays of the Emacs package ecosystem that you’ve probably used. If you’re reading this, chances are you already know (and probably use) Avy. So here’s a very short version from the documentation:

avy is a GNU Emacs package for jumping to visible text using a char-based decision tree.

You can call an Avy command and type in some text. Any match for this text on the frame (or all Emacs frames if desired) becomes a selection candidate, with some hint characters overlaid on top. Here I type in “an” and all strings in the frame that match it are highlighted:

Typing in one of the hints then jumps the cursor to that location. Here I jump to this sentence from another window:

Play by play
  1. Call avy-goto-char-timer
  2. Type in “se”. This shows hints for all matches with “se”, including “sentence”.
  3. Type in the hint char corresponding to “sentence”, which is g here.

Typical Avy usage looks something like this: Jump to a location that matches your text (across all windows), then jump back with pop-global-mark (C-x C-SPC). In a later section I go into more detail on jumping back and forth with Avy. Here is a demo of this process where I jump twice with Avy and then jump back in sequence:

Play by play
  1. Call avy-goto-char-timer
  2. Type in “demo”. There is only one candidate for this string, so Avy jumps to the other window.
  3. Type in “jump”. This shows hints for all matches with “jump”.
  4. Pick one of the matches. Avy jumps again, this time to the third window.
  5. Call pop-global-mark (C-x C-SPC) to jump back to the previous location.
  6. Call pop-global-mark (C-x C-SPC) again to jump back to the previous location.

At least that’s the official description. You can peruse the README for more information, but what I find mystifying is that…

…Avy’s documentation leaves out the best part

Avy handles filtering automatically and the selection is made through a char-based decision tree. Here’s how it fits into our three part interaction model.

Filter,

Before you call Avy every text character on your screen is a potential candidate for selection. The possibilites are all laid out for you, and there are too many of them!

You filter the candidate pool with Avy similar to how you would in the minibuffer, by typing text. This reduces the size of the pool to those that match your input. Avy provides dozens of filtering styles. It can: only consider candidates above/below point, only beginnings of words, only the current window (or frame), only whitespace, only beginnings of lines, only headings in Org files, the list goes on.

Filtering commands in Avy

Unit: Char Unit: Word or Symbol Unit: Line or other
avy-goto-char-timer avy-goto-word-0 avy-goto-line
avy-goto-char avy-goto-subword-0 avy-goto-end-of-line
avy-goto-char-2 avy-goto-symbol-1-above avy-goto-whitespace-end
avy-goto-char-in-line avy-goto-word-0-below avy-org-goto-heading-timer
avy-goto-char-2-below avy-goto-word-1-below avy-goto-whitespace-end-above
avy-goto-char-2-above avy-goto-symbol-1-below avy-goto-line-above
avy-goto-word-1-above avy-goto-whitespace-end-below
avy-goto-symbol-1 avy-goto-line-below
avy-goto-word-or-subword-1
avy-goto-subword-1
avy-goto-word-1
avy-goto-word-0-above

This is a crazy list to keep track of.

Filtering in Avy is independent of the selection method (as it should be), but there is a dizzying collection of filtering methods. I assume the idea is that the user will pick a couple of commands that they need most often and commit only those to memory.

Here’s the problem: We want to use our mental bandwidth for the problem we’re trying to solve with the text editor, not the editor itself. Conscious decision-making is expensive and distracting. As of now we need to decide on the fly between Isearch and Avy to find and act on things. If you use a fancy search tool like Swiper, Helm-swoop or Consult-line, you now have three options. Having a bunch of Avy commands on top is a recipe for mental gridlock. To that end, we just pick the most adaptable, flexible and general-purpose Avy command (avy-goto-char-timer) for everything.

(global-set-key (kbd "M-j") 'avy-goto-char-timer)

Further below I make the case that you don’t need to make even this decision, you can always use Isearch and switch to Avy as needed.

To be clear, this decision cost has to be balanced against the cost of frequent busywork and chronic context switching that Avy helps avoid. There is a case to be made for adapting Avy’s flexible filtering options to our needs, and the number of packages that offer Avy-based candidate filtering (for everything from linting errors to buffer selection) attests to this. We will examine this in depth in Part II.

But in this piece we are interested in a different, much less explored aspect of Avy.

Select,

Every selection method that Avy offers involves typing characters that map to screen locations. This is quite different from Isearch, where you call isearch-repeat-forward (C-s, possibly with a numerical prefix argument) or the minibuffer, where you navigate a completions buffer or list with C-n and C-p. Avy’s selection method is generally faster because it minimizes, by design, the length of the character sequences it uses, and we have ten fingers that can access ∼40 keys in O(1) time. The fact that we’re often looking directly where we mean to jump means we don’t need to parse an entire screen of gibberish. Unfortunately for this article, this means using Avy is much more intuitive than looking at screenshots or watching demos.

This excellent design leaves us with little reason to tinker with the selection phase: it’s sufficiently modular and accommodating of different filter and act stages. You can customize avy-style if you want to change the set or positions of characters used for selection. Here is an example of using simple words to select candidates:

We will pay more attention to the selection operation in part II as well.

Act!

This brings us to the focus of this article. The stated purpose of Avy, jumping to things, makes it sound like a (contextually) faster Isearch. But jumping is only one of many possibilities. Avy provides a “dispatch list”, a collection of actions you can perform on a candidate, and they are all treated on equal footing with the jump action. You can show these commands any time you use Avy with ?:

This means we are free to leverage Avy’s unique filtering and selection method to whatever action we want to carry out at any location on the screen. Our interaction model now ends in a block that looks something like this:

Additionally, Avy also defines a few commands that run different actions, like copying text from anywhere on screen:

Kill Copy Move
avy-kill-ring-save-whole-line avy-copy-line avy-move-line
avy-kill-ring-save-region avy-copy-region avy-move-region
avy-kill-region avy-transpose-lines-in-region
avy-kill-whole-line avy-org-refile-as-child

The problem with this approach is that it doesn’t scale. Each of these commands defines a full FilterSelectAct process, and we quickly run out of headspace and keyboard room if we want any kind of versatility or coverage. They’re also not dynamic enough: you’re locked into the pipeline and cannot change your mind once you start the command.

Folks love Vim’s editing model for a reason: it’s a mini-language where knowing M actions (verbs) and N cursor motions gives you M × N composite operations. This (M + N) → (M × N) ratio pays off quadratically with the effort you put into learning verbs and motions in Vim. Easymotion, which is Vim’s version of Avy3, has part of this composability built-in as a result. We seek to bring some of this to Avy, and (because this is Emacs) do a lot more than combining motions with verbs. We won’t need to step into Avy’s source code for this, it has all the hooks we need already.

Avy actions

The basic usage for running an arbitrary action with Avy is as follows:

  1. Call an Avy command. Any command will do, I stick to avy-goto-char-timer.
  2. Filter: Type in some text to shrink the candidate pool from the entire screen to a few locations.
  3. Act: Specify the action you want to run. You can pull up the dispatch help with ?, although you won’t have to if you set it up right, see Remembering to Avy.
  4. Select: Pick one of the candidates to run the action on.

Here are some things I frequently do with Avy. Note that you can do this on any text in your frame, not just the active window!

First, taking the annoyance out of some common editing actions with Avy. If you use Vim and Easymotion, you get the first few actions below for free:

A note about these demos
  • For clarity, I set Avy to desaturate the screen and to “pulse” the line during a few of these actions. These are not enabled by default. I also slowed down the operations by adding a delay to make them easier to follow. In actual usage these are instantaneous.

  • The keys Avy uses to dispatch actions on candidates are specified in avy-dispatch-alist.

  • We will also need to ensure that these keys don’t coincide with the ones Avy uses as selection hints on screen. Consider customizing avy-keys for this.

Kill a candidate word, sexp or line

Killing words or s-expressions is built-in. I added an action to kill a line. In this demo I quickly squash some typos and delete a comment, then remove some code in a different window:

Play by play
  1. Call avy-goto-char-timer.
  2. Type in “is”. This shows hints for all matches for “is”.
  3. Call avy-action-kill with k
  4. Select one of the duplicate “is” occurrence. This deletes it.
  5. Repeat steps 1-4 to delete a redundant “and”.
  6. Call avy-Goto-Char-Timer.
  7. Type in “key” to filter matches for “key”.
  8. Call avy-action-kill-whole-line with K.
  9. Select the commented line, this removes the line.
  10. Repeat steps 1-4, this time selecting “(” in the other window and killing the function definition.
  11. Repeat steps 7-9, this time selecting “ad” in the other window and killing the “(advice-add …)” line.
(defun avy-action-kill-whole-line (pt)
  (save-excursion
    (goto-char pt)
    (kill-whole-line))
  (select-window
   (cdr
    (ring-ref avy-ring 0)))
  t)

(setf (alist-get ?k avy-dispatch-alist) 'avy-action-kill-stay
      (alist-get ?K avy-dispatch-alist) 'avy-action-kill-whole-line)

Yank a candidate word, sexp or line

Copy to the kill-ring or copy to point in your buffer. In this demo I copy some text from a man page into my file:

Play by play
  1. Call avy-goto-char-timer.
  2. Type in “[". This filters to all matches for “[” in the frame.
  3. Call avy-action-yank, bound to y.
  4. Select the match corresponding to “[big-cache]". This text is copied to the buffer from the other window.
  5. Call avy-goto-char-timer.
  6. Type in “de”. This filters to matches that include “demuxer”.
  7. Call avy-action-yank-whole-line, bound to Y.
  8. Select one of the matches. The line is copied to the buffer.
  9. Fix indentation with just-one-space, bound to M-SPC by default.
(defun avy-action-copy-whole-line (pt)
  (save-excursion
    (goto-char pt)
    (cl-destructuring-bind (start . end)
        (bounds-of-thing-at-point 'line)
      (copy-region-as-kill start end)))
  (select-window
   (cdr
    (ring-ref avy-ring 0)))
  t)

(defun avy-action-yank-whole-line (pt)
  (avy-action-copy-whole-line pt)
  (save-excursion (yank))
  t)

(setf (alist-get ?y avy-dispatch-alist) 'avy-action-yank
      (alist-get ?w avy-dispatch-alist) 'avy-action-copy
      (alist-get ?W avy-dispatch-alist) 'avy-action-copy-whole-line
      (alist-get ?Y avy-dispatch-alist) 'avy-action-yank-whole-line)

Note that Avy actually defines separate commands for this: avy-copy-line and avy-copy-region to copy lines and regions from anywhere in the frame. These are a little faster since they have the action stage baked into the function call. You might be better served by these. But we want to avoid the mental burden of remembering too many top level commands, so we work in two simpler stages: call avy-goto-char-timer (to filter and select) and then dispatch on our selected candidate as we see fit.

Move a candidate word, sexp or line

Avy calls this “teleport”, I call it “transpose”, either way it’s bound to t. In this demo I move text around the buffer… without moving (much) around the buffer:

Play by play
  1. Make some room, type in a space.
  2. Call avy-goto-char-timer.
  3. Filter to candidates that begin with “(".
  4. Press t to run avy-action-teleport
  5. Select the candidate that says “(parametric forcing)". It is moved over (transposed) to where the point is.
  6. Jump to where it says “DOWNLOADED” in the window with avy-goto-char-timer. This is the only match for the input “down”, so Avy jumps there automatically. You could also just isearch-backwards here.
  7. Call avy-goto-char-timer.
  8. Filter to candidates matching “the”.
  9. Press T to run =avy-action-teleport-line~.
  10. Select a candidate line (the one just below the image). It is moved over (transposed) to where the point is.
(defun avy-action-teleport-whole-line (pt)
    (avy-action-kill-whole-line pt)
    (save-excursion (yank)) t)

(setf (alist-get ?t avy-dispatch-alist) 'avy-action-teleport
      (alist-get ?T avy-dispatch-alist) 'avy-action-teleport-whole-line)

Zap to a candidate position

This is built-in and bound to z by default:

Play by play
  1. Call avy-goto-char-timer
  2. Type in “in”. This shows hints for all matches with “in”, including “In Emacs…".
  3. Press z to run avy-action-zap.
  4. Select a candidate char, in this case “In Emacs…". The text between point and the candidate is killed.

Mark a candidate word or sexp

Also built in, m by default. This isn’t different from jumping to the candidate using Avy and calling mark-sexp, but it is more convenient:

Play by play
  1. Call avy-goto-char-timer.
  2. Type in text to filter with, in this case just “(".
  3. Press m to run avy-action-mark.
  4. Select a candidate word or sexp, in this case ("~/.local/share").
  5. Repeat steps 1 to 4 twice to mark other candidates: (data_directory... and RotatingFileHandler

Mark the region from point to a candidate

Avy sets the mark before it jumps, so you could use C-x C-x to activate the region, but this saves you the trouble.

Play by play
  1. Call avy-goto-char-timer.
  2. Type in text to filter with, in this case “')".
  3. Press SPC to run avy-action-mark-to-char.
  4. Select a candidate char. This marks the region from point to the char and moves the point.
  5. Call avy-goto-char-timer.
  6. Type in text to filter with, in this case just a series of spaces.
  7. Press SPC to run avy-action-mark-to-char.
  8. Choose a candidate (series of spaces) that begins a line. This marks the region from point to the line.
(defun avy-action-mark-to-char (pt)
  (activate-mark)
  (goto-char pt))

(setf (alist-get ?  avy-dispatch-alist) 'avy-action-mark-to-char)

Next, some contextual actions automagicked by Avy:

ispell a candidate word

This is built-in, bound to i by default.

Play by play
  1. Call avy-goto-char-timer (or any other Avy jump command)
  2. Type in “can”, this highlights matches for “candidate” (and its misspellings)
  3. Press the dispatch key for avy-action-ispell, set to i by default.
  4. Select one of the matches, in this case the misspelled “canddidate” match.
  5. This runs ispell-word on the selection.
  6. Pick the correct spelling.
  7. Call avy-goto-char-timer again.
  8. Type in “te”, this highlights the match for “teh” (among others).
  9. Press the dispatch key for avy-action-ispell
  10. Select the candidate, in this case the “teh” match.
  11. This runs ispell-word again, and “teh” can be corrected.

You can replace avy-action-ispell (built-in) with a version that automatically picks the top correction for a word, automating the process:

(defun avy-action-flyspell (pt)
  (save-excursion
    (goto-char pt)
    (when (require 'flyspell nil t)
      (flyspell-auto-correct-word)))
  (select-window
   (cdr (ring-ref avy-ring 0)))
  t)

;; Bind to semicolon (flyspell uses C-;)
(setf (alist-get ?\; avy-dispatch-alist) 'avy-action-flyspell)

Define a word

I use the dictionary package for Emacs, and I’m lazy about it:

Play by play
  1. Call avy-goto-char-timer (or any other Avy jump command)
  2. Type in “non”, this highlights matches for “nonpareil” (among others)
  3. Press the dispatch key for avy-action-define, set to = here
  4. Select the candidate, in this case one of the “nonpareil” matches.
  5. This produces the buffer with the definition of “nonpareil”.
  6. Call scroll-other-window (C-M-v) to scroll the dictionary window.
  7. Call avy-goto-char-timer again.
  8. Type in “fi”, this highlights the match for “finch” (among others). Note that this match is in another buffer, the one with the definition. We did not have to switch buffers.
  9. Press the dispatch key for avy-action-define
  10. Select the candidate, in this case the “finch” match.
  11. This produces the buffer with the dictionary definition of “finch”.
;Replace your package manager or preferred dict package
(package-install 'dictionary)

(defun dictionary-search-dwim (&optional arg)
  "Search for definition of word at point. If region is active,
search for contents of region instead. If called with a prefix
argument, query for word to search."
  (interactive "P")
  (if arg
      (dictionary-search nil)
    (if (use-region-p)
        (dictionary-search (buffer-substring-no-properties
                            (region-beginning)
                            (region-end)))
      (if (thing-at-point 'word)
          (dictionary-lookup-definition)
        (dictionary-search-dwim '(4))))))

(defun avy-action-define (pt)
  (save-excursion
    (goto-char pt)
    (dictionary-search-dwim))
  (select-window
   (cdr (ring-ref avy-ring 0)))
  t)

(setf (alist-get ?= avy-dispatch-alist) 'dictionary-search-dwim)

Look up the documentation for a symbol

Play by play
  1. Call avy-goto-char-timer
  2. Type in text to filter with, in this case “pc”.
  3. Press H to run avy-action-helpful.
  4. Select a candidate phrase, in this case “pcase-lambda”. This pulls up a documentation buffer for this symbol.
  5. scroll-other-window with C-M-v to scroll the help buffer.
  6. call avy-goto-char-timer
  7. Type in text to filter with, in this case “ma”.
  8. Press H to run avy-action-helpful.
  9. Select a candidate phrase, in this case “macroexp-parse-body”. Note that this is matched in the other (help) window. This pulls up the documentation for this symbol.
  10. Repeat steps 5-9 to find the documentation of another symbol, in this case memq.
;Replace with your package manager or help library of choice
(package-install 'helpful)

(defun avy-action-helpful (pt)
  (save-excursion
    (goto-char pt)
    (helpful-at-point))
  (select-window
   (cdr (ring-ref avy-ring 0)))
  t)

(setf (alist-get ?H avy-dispatch-alist) 'avy-action-helpful)

Google search for a word or sexp4

You’ll need an Emacs feature that can search Google for you. There are several. I use a CLI program named Tuxi for this, and it’s pretty handy:

Play by play
  1. Call avy-goto-char-timer (or any other Avy jump command)
  2. Type in “ema”, this highlights matches for “Emacs” (among others)
  3. Press the dispatch key for avy-action-tuxi, set to C-= here
  4. Select the candidate, in this case one of the “Emacs” matches.
  5. This produces the buffer with the Google’s description of Emacs.
  6. Call avy-goto-char-timer again.
  7. Type in “vi”, this highlights the match for “Vi” (among others). Note that this match is in another buffer, the one with the Google result. We did not have to switch buffers.
  8. Press the dispatch key for avy-action-tuxi
  9. Select the candidate, in this case the “Vi” match.
  10. This produces the buffer with Google’s description of Vi.
  11. Repeat steps 6-10 but selecting the string “POSIX” instead.

Now: We could continue populating avy-dispatch-alist with functions to do increasingly arcane contextual actions, but let’s take a step back. We want a list of easily callable actions on pieces of semantically classified buffer text… now where have we seen something like this before?

Avy + Embark: Any action, anywhere

Avy and Embark plug into each other like LEGO blocks. Here are a couple of examples:

Highlight occurrences

In this demo I highlight some keywords in a busy LaTeX document, then visit the bibliography entry of a citation key with Avy and Embark, without ever manually moving the cursor:

Play by play
  1. Call avy-goto-char-timer (or any other Avy jump command)
  2. Type in “flo” to filter matches that include “Floquet”
  3. Run avy-action-embark with o.
  4. Select one of the matches for “Floquet”. This runs Embark on the match.
  5. Select the embark-toggle-highlight action with H.
  6. Repeat 1-5 to highlight “Parametric”.
  7. Call avy-goto-char-timer again.
  8. Type in “na” to match one of the citation keys (among others)
  9. Run avy-action-embark with o.
  10. Select the citation key match. This runs Embark on it.
  11. Choose the bibtex-action to visit the Bib file, bound to e by the bibtex-actions package.

Run a skein through Emacs’ help systems

In this demo I explore my way through a package with Avy and Embark, threading help, apropos and customization buffers, again without manually moving the cursor.

Play by play
  1. Call avy-goto-char-timer.
  2. Type in text to filter with, in this case “root”.
  3. Run avy-action-embark with o.
  4. Select “project-root”, one of the matches. This runs Embark on the match.
  5. Press h, which makes Embark run describe-symbol on the match. This opens up a help buffer for the function. (Note: we bound a help command to Avy earlier, we could have used that.)
  6. Press C-M-v (scroll-other-window) to scroll the help buffer.
  7. Call avy-goto-char-timer again.
  8. Type in text to filter with, in this case “proj”.
  9. Run avy-action-embark with o.
  10. Select “project-x”, which is one of the matches. This runs Embark on the match.
  11. Call embark-cycle to change the target from a file (named “project-x”) to a library (named “project-x”)
  12. Press h, which makes Embark run finder-commentary on the project-x library. This opens a buffer with some commentary.
  13. Repeat the previous steps to run Embark on “project-x” again. This time, run the apropos-library action with a in Embark. This opens an apropos buffer.
  14. Repeat the previous steps to run Embark again, this time on the symbol “project-x-local-identfier”.
  15. Choose the customize-variable action with u in Embark. This opens a customization buffer for the variable project-x-local-identifier.

A division of responsibility

We save ourselves a lot of redundancy and reuse muscle memory here. Avy provides its unique means of filtering and Embark does what it does best, run actions! The intermediate job of candidate selection is shared between Avy and Embark: Avy specifies the general location of the candidate, and Embark figures out the semantic unit at that position on which to act. The fact that the FilterSelectAct process is helpfully chunked this way by Avy makes the elisp required to integrate the two completely trivial5:

(defun avy-action-embark (pt)
  (unwind-protect
      (save-excursion
        (goto-char pt)
        (embark-act))
    (select-window
     (cdr (ring-ref avy-ring 0))))
  t)

(setf (alist-get ?. avy-dispatch-alist) 'avy-action-embark)

Note that if you don’t like the candidate that Embark picks as the unit to act on, you can call embark-cycle to cycle through the other targets.

All that, and we didn’t even move the point.6

Avy + Isearch: Seek, then jump

Isearch and Avy have different strengths: Avy jumps quickly to any visible element in any window, Isearch to any matching candidate in this buffer. Avy is faster when you want to cover some distance in a jump, Isearch when you’re moving a small distance or a very large one. Avy is useful when your eyes are already on a target, Isearch when you’re looking for one. But you don’t have you choose. You can handily combine the two by restricting Avy’s candidate pool to Isearch candidates: now you can start Isearch and finish with Avy:

(define-key isearch-mode-map (kbd "M-j") 'avy-isearch)

Again, consciously deciding which of the two commands to call every time is a bad idea. It’s not a bad idea to always Isearch and switch to Avy when necessary:

Play by play
  1. Start Isearch with C-s. In the video I switched to isearch-regexp with M-r.
  2. Type in a phrase to search for. In the video I typed in a regexp that ends in “-region”.
  3. Navigate Isearch matches with C-s, recentering the screen with C-l if necessary.
  4. Call avy-goto-char-timer. The candidate pool limits to the Isearch matches.
  5. Pick an Isearch match to jump to as before.

At least, that’s the usual pitch.

For us, however, “jump” in that description is replaced with “act”. We can act on any visible Isearch candidate with one of the above actions. Kill text between two isearch matches? Copy the previous line that contains a word to the current location? Check. Essentially we filter with Isearch and select and Act with Avy, indirectly decoupling Filter from the other two actions in Isearch.

When Avy is too clever

This usage pattern has a failure mode. When there’s a single match, Avy jumps to the location and does not offer any actions. Oops.

While it’s possible to force Avy to show a selection char/hint for a single match, the default DWIM behavior is usually desirable. There are two options:

  • Filter candidates conservatively, for example by typing in a single character. Using avy-goto-char or avy-goto-char-2 will almost always result in more than one match, preventing this problem. If you use one of the timer-based Avy commands, you can vary how much text to filter by on the fly.

  • Carry out the action the old-fashioned way after jumping, then jump back by popping the mark Avy sets. You can do this with the default set-mark-command (C-u C-SPC)7. You can do this for most commands that cause the point to jump, including Isearch. Vim users have the jumplist, accessed with C-o, and the changelist, accessed with g;.

    In this demo I jump twice with Avy to edit text and then chain jump my way back to where I started:

    Play by play
    1. Call avy-goto-char-timer and jump to a candidate (or end up there by accident)
    2. Make your edits (or not).
    3. Call set-mark-command with a prefix arg (C-u C-SPC) to jump back. You can chain these jumps.

What’s the Point, anyway

This section is for the pedants.

I’ve been using “point” and “cursor” interchangeably in this article. Yes, I’m aware of the distinction.

One of the illustrated advantages of using Avy to filter and select text to run actions on is that you can do it without moving the cursor. But as the above code snippets make clear with their save-excursion blocks, we do move the point, mostly just invisibly. The point is where the “gap” in Emacs’ gap-buffer data structure is located, so Emacs commands are all oriented around acting on the point, and usually more efficient when doing so.

Yes: it’s much faster to run an Avy action on some text in a different window than it is to call other-window, then Isearch to the text, run an action and switch back. But to me, the Point (har) is primarily a useful abstraction for writing elisp. The real advantage of Avy is in how it lets me think about the contents of the entire frame in the powerful FilterselectAct paradigm. The fact that you can do this without the mental context switch involved in expressly navigating around the window or frame is a bonus.

Remembering to Avy

In some ways, using Avy to jump around the screen is like using a mouse. You could make the case, quite successfully, that the mouse is faster here and thus preferable. This unfavorable comparison evaporates when you add your dispatch actions into the mix. Yanking a line from another window or running goto-definition on a symbol on the other edge of the screen is much faster with Avy than with the mouse selection/right-click business. And this is without taking into account the disruptive effect of frequent context switching, the reason to prefer all keyboard navigation (or all mouse, when possible) in the first place.

The smell test

However, using Avy actions is a new way of interacting with text on your screen even if you already use Avy to jump around. To remember to use Avy actions or find new ones, I look for “smells” in my day-to-day Emacs usage:

  • Switching windows multiple times to land my cursor on some text
  • Isearching through more than three matches to jump to the right one
  • Moving the point a long distance to run a lookup command
  • Activating the mark manually (C-SPC) all the time
  • Jumping to locations to delete single words

Buried in keymaps

The other sense of “remembering to Avy” is that piling a new abstraction onto simple text editing means you have to learn a new keymap. Emacs already has too many of those!

This is true. But the effort is greatly mitigated by a choice of keys that is sensible to you. In the above code snippets, I made choices that mimic my Emacs’ keybindings (which are close to the default ones) so I don’t have to remember anything new:

Action Avy keybinding Emacs keybinding Emacs Default?
Kill k, K (line) C-k Yes
Copy w, W (line) M-w Yes
Yank y, Y (line) C-y Yes
Transpose t, T (line) C-t, M-t etc Yes
Zap z M-z Yes
Flyspell ; C-; Yes
Mark m m in special buffers Yes
Activate region SPC C-SPC Yes
Dictionary = C-h = No
Google search C-= C-h C-= No
Embark o C-o No

You can go beyond the mnemonic and simply reuse the same keybindings you use in regular editing, trading off a slightly longer key sequence for maximally reusing your muscle memory. If you’re an Embark user, you don’t even need the above keys, just one to call embark-act.

The missing pieces

There are two common editing actions that still require manually moving the point elsewhere, perhaps to another window:

  • Searching or jumping to the contents of other windows beyond the confines of the screen. This has a simple solution:

    Isearching in other windows
    (defun isearch-forward-other-window (prefix)
        "Function to isearch-forward in other-window."
        (interactive "P")
        (unless (one-window-p)
          (save-excursion
            (let ((next (if prefix -1 1)))
              (other-window next)
              (isearch-forward)
              (other-window (- next))))))
    
    (defun isearch-backward-other-window (prefix)
      "Function to isearch-backward in other-window."
      (interactive "P")
      (unless (one-window-p)
        (save-excursion
          (let ((next (if prefix 1 -1)))
            (other-window next)
            (isearch-backward)
            (other-window (- next))))))
    
    (define-key global-map (kbd "C-M-s") 'isearch-forward-other-window)
    (define-key global-map (kbd "C-M-r") 'isearch-backward-other-window)
    

    In keeping with C-M-v to scroll the other window, you can Isearch the other window with C-M-s without switching to it8. If you’re feeling adventurous, replace (other-window next) in the above functions with (ace-window).

    You can call Avy from Isearch as before, to run actions on essentially any text in the other-window’s buffer.

  • Copying regions of text. This has an Avy-based solution: avy-copy-region. I promised at the beginning, however, that you would only need to call one Avy command. For now you would do this the boring way, using Avy only to make shorter jumps or using Isearch+Avy. The more elispy solution will have to wait until part II of this series.

This post primarily concerned itself with the Act part as it connects with the ideas in the the previous one about ways to use Embark. But Avy is composed of modular pieces that makes it suitable for a wide variety of FilterSelect applications as well. In part II of this series, we will dig into the Avy API and see how to create unique commands.


  1. Technically there’s also sorting the candidates, which I lump in with filtering in this breakdown. ↩︎

  2. Isearch combines filter/search/act into a single command for efficiency. Even so, it’s a very cleverly designed library for Emacs with support for many actions. Folks who replace Isearch entirely with Swiper or similar commands are missing out. ↩︎

  3. Or perhaps Avy is Emacs’ version of Easymotion. ↩︎

  4. Why would you want to google an s-expression? The most common reason is when the sexp is a hyphenated phrase, like Fink-Nottle. ↩︎

  5. Thanks to Omar Antolin for the improvement to this function. ↩︎

  6. Technically this is false, since we use save-excursion in all the avy action functions. The real accomplishment here is that I got almost 4000 words into this piece before mentioning Embark. ↩︎

  7. If you end up in a different window you can pop the global mark instead with pop-global-mark (C-x C-SPC). There are also external packages for this now, like dogears. ↩︎

  8. C-M-s is bound to isearch-forward-regexp by default. You can continue to access it with M-r in the Isearch map, i.e. C-s M-r. ↩︎

Add a comment, or comment via email





Aankhen
2021-10-25
Are you a wizard, sir? This post and the one on Embark have changed how I see Emacs (even if it’ll be a few months before all this becomes muscle memory), and shown me how to deal with a few perennial stumbling blocks. Thank you very much! I’m looking forward to the second part.
Omer
2021-11-08

I think we should somehow be using avy in M-x completion windows.

Consider this, there is a command named avy-execute-extended-command-timer (an avy-goto-char-timer counterpart for completion buffers)

When we invoke this command, we have 1-2 seconds to filter through the execute-extended-command (M-x) and then the completions appear as they have Avy chars on them. So then we press the char in order to execute the command.

Jesse
2021-11-12

This post was sooooo good. Thank you! Had no idea how easy to use avy-actions are, and I use avy all the time.

Really looking forward to part 2!

P.s., the last link is broken.

Karthik
2021-11-13
@Omer: Such a command is simple to write, but the details will depend on the completion system you’re using. For instance, Vertico has the vertico-quick extension, which adds this feature. Since there are a few M-x candidates on screen at any time, you don’t need a timer, you can select or act on a candidate directly.
Karthik
2021-11-13
@Jesse: Cheers. Thanks for spotting the broken link, it’s fixed now.