2019-07-01 Syntax-aware navigation, keyboard macros, sleeping Emacs and interactive functions

Warning: today’s post is a bit of a “stream of consciousness” in that it describes several unrelated things – or rather, several things related only by the fact that I learned them while solving one particular problem.

A few days ago I had an interesting Emacs-related problem. I wanted to record and use a keyboard macro which would find a certain TeX one-parameter macro (say, \todo) and comment it out.

The tricky part was that it was not at all guaranteed that this macro would be on a line on its own, so “finding the string \todo, going to the beginning of the line and inserting a percent character” won’t work.

All is not lost, however. Emacs has the excellent and extremely useful C-M-f command (forward-sexp), which does a few useful things. First of all, when the point is on an opening parenthesis (of any kind, so also a bracket or a curly brace, for instance), it jumps to the closing one, taking possible nested parens into account. Secondly, when on a beginning of a word or a string constant, it jumps to its end.

And now the way to go is clear: what I want to record is:

  1. Search for \todo.
  2. Go back to the backslash.
  3. Activate the mark.
  4. Press C-M-f twice (or once with a prefix argument of 2).
  5. Press M-; (comment-dwim, which calls comment-region if the region is active).

That worked well, but I had one problem with it: when the next occurrence of the \todo was not visible, I felt I was losing control and I was unsure whether my macro did what it should do. (And sometimes indeed it didn’t work – when the argument of \todo had unbalanced parens, which tripped forward-sexp).

What to do? Well, my first idea was to make Emacs sleep for a second before firing C-M-f. I quickly found the sleep-for function, only to discover that it is not a command. I then sent an email to the emacs-devel mailing list, asking whether it could be good idea to make sleep-for interactive.

Well, the Emacs community did not disappoint. I learned a bit from a few answers there.

The TL;DR is that it was not a good idea. But here are the things I learnt.

First of all, nothing stops me from saying M-: (sleep-for 1) RET in my keyboard macro. Fair enough.

Then, I learned that sleep-for does not update the display anyway, so what I really needed was sit-for (which does that, and a lot more).

The next thing (and probably the most valuable of all this) was that I could attain my goal in a completely different way: what I could do is to start recording my macro with point already on the beginning of \todo, and isearch for the next occurrence at the end of the macro. Although in this particular use-case this might not be the best idea (see my comment about unbalanced parens above), this technique is a very useful thing I tend to forget about from time to time.

(In fact, it would be even better if I could include a “break” in the keyboard macro, so that Emacs would pause during its execution. It would be even better if it could then e.g. ask if I want to continue or not. Emacs being Emacs, such a feature exists. It is so cool I am going to write a separate blog post about it. This I learned from the manual, though, not from the discussion.)

The final takeaway from the thread was something that blew my mind. (Not that it is extremely useful, but still.) It turns out that I can change a function into a command without actually redefining it. Here is how it is done:

(put 'sleep-for 'interactive-form
     '(interactive "nSleep for: "))

What is going on here? It turns out that every symbol in Elisp has something called a symbol’s property list, and Emacs puts a lot of stuff for various symbols there by defaultinteractive-form is just one of about two dozen! (BTW, from a cursory read I get the impression that it might be possible to have an interactive alias for a non-interactive function. How cool is that?)

Even after about two decades of heavy use, Emacs does not stop to astonish me…

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryTeX