(interzone)

Keep your input, change the target

Switch commands mid-search with embark-become and carry on

By Bruno Cardoso on

I rely on a common set of commands for lookup and jumping around. And more often than not, I’m searching for something that I know exists somewhere, but it’s not coming up in the results. Because I’m typing in the wrong command interface.

And that’s precisely the use case for embark-become.

I used embark mostly for jumping to commands’ definitions from within M-x and killing multiple buffers with embark-act-all. Re-reading its documentation, I found it already solves a problem I’ve been tackling the hard way.

My partial solution was to keep some related commands on each other’s keymaps, using consult’s consult-customize macro. When I’m in consult-outline, for example, I use the same keybinding to escalate my input to consult-line, and from there to consult-line-multi, which is very handy.

Still, that doesn’t work as well for non-consult commands, and my workarounds were getting too involved and fragile. More importantly, they don’t prevent me from invoking the wrong keybinding and having to start over.

What embark-become does is find related commands by looking for the current minibuffer command in the keymaps from embark-become-keymaps. It then manages to replace the current command with another one while preserving the input you’ve just typed.

By becoming aware of that, I was able to erase many lines from my configuration and replace them with a custom keymap:

(defvar-keymap my/embark-become-map
    :doc "My common lookup commands."
    "b" #'consult-buffer
    "f" #'find-file
    "x" #'execute-extended-command
    "h" #'consult-org-hop
    "q" #'my/org-ql-find
    "s" #'describe-symbol
    "l" #'consult-line
    "L" #'consult-line-multi
    "o" #'consult-outline
    "i" #'consult-imenu
    "I" #'consult-imenu-multi
    "c" #'my/org-capture-note)

(add-hook 'embark-become-keymaps 'my/embark-become-map)

I also bound embark-become to C-M-. in the minibuffer-local-map.

Now, whenever I’m typing something inside one of those commands, I can quickly switch to any other and keep the current input.

Custom commands #

Three of the commands above are specific to my usual workflow.

consult-org-hop is from a package of mine, org-hop. I use it daily to hop to any Org heading from my files by searching for its title, outline path, or tags.

When I need to find something that may be present in the contents of a heading, I rely on org-ql-find:

(defun my/org-ql-find (&optional arg)
  "Run `org-ql-find' in Org mode buffers.
When ARG is non-nil, search only in current buffer."
  (interactive "P")
  (let ((org-ql-default-predicate 'rifle))
    (if (and arg (derived-mode-p 'org-mode))
        (org-ql-find (current-buffer))
      (org-ql-find (org-buffer-list)))))

If I’m still unable to find what I want, or when the results aren’t really related with the topic I’m working on, I create a new note:

(defun my/org-capture-note (input)
  "Run Org capture from current minibuffer INPUT."
  (interactive "sNew note title: ")
  (org-capture nil "n")
  (insert (or input ""))
  (goto-char (point-max)))

This command prompts for a note title, with the initial contents from the minibuffer input. After I adjust the writing, it runs org-capture and inserts the title where the template “n” leaves the cursor on in a regular capture. Finally, it moves the point to the end of the buffer, where I can proceed to write the new note’s contents.

Final thoughts #

With this simple setup, invoking a command by mistake becomes just a matter of reorientation, not interruption.

You keep moving forward by changing what you are matching against, and not what you are typing for.