This chapter shows how to get different commands to work together by saving information in one command and retrieving it in another. The simplest way to share information is to create a variable and store a value in it. We’ll certainly do that in this chapter. For instance, we’ll store the current buffer position and reuse it in a later command. But we’ll also learn some more sophisticated ways to preserve state, notably markers and symbol properties. We’ll combine these techniques with information about buffers and windows to write a set of functions that allow you to “undo” scrolling.
You’re deep into editing some complicated Lisp code. You’re concentrating, juggling the tenuous connections between the conceptual structures in your brain and the glyphs that represent them on the screen. You’re in a particularly tricky part when you notice a typo a few characters to the left. You mean to press C-b C-b C-b to back up and correct it, but instead—horrors!—you press C-v C-v C-v, paging the Emacs window three times, ending up light years away from the code you were editing. Your mental context is ruined as you try to figure out where the cursor was before your mistake, and why, and what you were doing there. As you scroll, or search, or cycle through the mark-ring or the undo-list trying to get back to where you were, you forget about that original typo you were trying to correct, and much later it turns into a bug in your code that takes hours to find.
Emacs hasn’t helped in this instance, it has hindered. It has made it too easy to get lost in your document and too hard to find your way back. Although Emacs has an extensive undo facility, it only allows you to undo changes. You can’t undo simple navigation.
Suppose we could alter C-v (the scroll-up
command[16]) in such a way that when you press it, Emacs thinks, “Maybe the user is pressing C-v in error, so I’ll record some ‘undo’ information in case it’s needed.” Then we could write another function, unscroll
, which undoes the effects of the latest scroll. Getting lost should therefore cause no more disruption to your mental context than it takes to remember the keybinding for unscroll
.
Actually, that’s not quite good enough. If you press several C-vs in a row, one call to unscroll
should undo them all, not only the last one. This means that only the first C-v in a sequence should memorize the starting location. How can we arrange for this to happen? Somewhere in our C-v code, before we memorize the starting location, we have to test either (a) that the next command will be a call to scroll-up
, or (b) that the previous command wasn’t a call to scroll-up
. Obviously, (a) is impossible: we can’t know the future. Fortunately, (b) is easy: Emacs maintains a variable for this purpose called last-command
. This variable is the first mechanism we’ll use to communicate information from one command to a later one.
Now the only question remaining is: how can we attach this extra code to the scroll-up
command? The advice facility is ideal for this purpose. Recall that a piece of advice can run before or after the advised function. In this case, we’ll need before advice, because it’s only before scroll-up
runs that we know the starting location.
We’ll start by setting up a global variable, unscroll-to
, which will hold the “undo” information, which is simply the position in the buffer to which unscroll
should move the cursor. We’ll use defvar
to declare the variable.
(defvar unscroll-to nil "Text position for next call to 'unscroll'.")
Global variables don’t need to be declared. But there are some advantages to declaring variables with defvar
:
Using defvar
allows a docstring to be associated with the variable, in the same way that defun
allows a docstring to be associated with a function.
A default value for the variable can be given. In this case, the default value for unscroll-to
is nil
.
Setting a variable’s default value with defvar
is different from setting a variable’s value with setq
. Instead of unconditionally assigning the value to the variable like setq
does, defvar
assigns the value only if the variable does not yet have any value.
Why is this important? Suppose your .emacs file contains the line
(setq mail-signature t)
meaning that when you send a mail message from within Emacs, you wish to append your .signature file to it. When you start Emacs, mail-signature
gets set to t
, but the Lisp file that defines the mail-sending code, sendmail, has not yet been loaded. It’s loaded on demand when you first invoke the mail
command. When you do, Emacs executes this line from the sendmail Lisp file:
(defvar mail-signature nil ...)
This says that nil
is a default initial value for mail-signature
. But you’ve already given mail-signature
a value, and you wouldn’t want loading sendmail to override your setting. On the other hand, if your .emacs didn’t specify any value for mail-signature
, you would want this value to be in effect.
A variable declaration using defvar
can be found by the various tag-related commands. Tags are a way to quickly find variable and function definitions in a programming project. Emacs’s tag facilities, such as the find-tag
command, can find anything created with the def…
functions (defun, defalias, defmacro, defvar, defsubst, defconst, defadvice
).
When you byte-compile the code (see Chapter 5), the byte-compiler emits a warning for each variable it encounters that hasn’t been declared with defvar
. If all your variables are declared, then you can use the warnings to find places where you’ve mistyped the name of a variable.
Let’s define the value of unscroll-to
to be the position in the text where the cursor was before the latest sequence of scroll-up
s. The position of the cursor in the text is the number of characters from the beginning of the buffer (counting from 1) and is called point or the point. The value of point at any moment is given by the function point
.
(defadvice scroll-up (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (if (not (eq last-command 'scroll-up)) (setq unscroll-to (point))))
The body of this advice works as follows:
The function eq
takes two arguments and tells whether they are identical. In this case, the arguments are the value of the last-command
variable, and the literal symbol scroll-up
. The value of last-command
is the symbol naming the last command that the user invoked (usually—see the section on Using this-command later in this chapter).
The result of the call to eq
is passed to not
, which inverts the truth value of its argument. If nil
is passed to not
, the result is t
. If anything else is passed to not
, the result is nil
.[17]
If the result of the call to not
is true—i.e., if last-command
is not the symbol scroll-up
—then the variable unscroll-to
is set to the current value of point by calling the function point
with no arguments.
Now it should be easy to define unscroll
:
(defun unscroll () "Jump to location specified by 'unscroll-to'." (interactive) (goto-char unscroll-to))
The function goto-char
moves the cursor to the given position.
There’s something unsatisfactory about this solution. After an unscroll
, the cursor is restored to its correct location, true, but the screen may look very different from the way it appeared before the C-v excursion. For example, I may be editing a line of code that is near the bottom of the Emacs window when I mistakenly press C-v C-v C-v. I’ll immediately invoke unscroll
, but even though the cursor goes back where it belongs, the line in question may now appear in the middle of the window.
Since our goal is to minimize the disruption caused by unintended scrolling, we’d really like to restore not only the location of the cursor, but also the appearance of the window with respect to which lines are visible where.
Saving the value of point is no longer sufficient, therefore. We must also save a value representing what’s visible in the current window. Emacs provides several functions describing what’s visible in a window, such as window-edges
, window-height
, and current-window-configuration
. For now we’ll only use window-start
which, for a given window, yields the buffer position that is the first visible character (i.e., the upper-left corner) in the window. We’re just adding a little more information to be preserved between commands.
Updating our example is straightforward. First we replace our declaration of the variable unscroll-to
with two new variables: one containing the saved value of point, and one containing the saved position of the first visible character in the window.
(defvar unscroll-point nil "Cursor position for next call to 'unscroll'.") (defvar unscroll-window-start nil "Window start for next call to 'unscroll'.")
Next we update the advice on scroll-up
and unscroll
to set and use these two values.
(defadvice scroll-up (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (if (not (eq last-command 'scroll-up)) (progn (setq unscroll-point (point)) (setq unscroll-window-start (window-start))))) (defun unscroll () "Revert to 'unscroll-point' and 'unscroll-window-start'." (interactive) (goto-char unscroll-point) (set-window-start nil unscroll-window-start))
Since the advice is still named remember-for-unscroll
, this advice replaces the previous advice, which was identically named.
The function set-window-start
sets the window-start position in the same way that goto-char
sets the position of the cursor. However, set-window-start
takes two arguments. The first argument is the window whose start position is being set. If nil
is passed as the first argument (as in this example), set-window-start
defaults to the currently selected window. (Window objects for passing to set-window-start
can be obtained from such functions as get-buffer-window
and previous-window
.)
There’s one more piece of information we might like to save for unscrolling purposes, and that’s the window’s hscroll, the number of columns by which the window is scrolled horizontally, normally zero. We’ll add yet another variable for storing it:
(defvar unscroll-hscroll nil "Hscroll for next call to 'unscroll'.")
then we’ll update unscroll
and the scroll-up
advice again to include calls to window-hscroll
(which reports the window’s current hscroll) and set-window-hscroll
(which sets it):
(defadvice scroll-up (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (if (not (eq last-command 'scroll-up)) (setq unscroll-point (point) unscroll-window-start (window-start) unscroll-hscroll (window-hscroll)))) (defun unscroll () "Revert to 'unscroll-point' and 'unscroll-window-start'." (interactive) (goto-char unscroll-point) (set-window-start nil unscroll-window-start) (set-window-hscroll nil unscroll-hscroll))
Notice that in this version of the scroll-up
advice, the progn
call:
(progn (setq ...) (setq ...))
has been turned into a single setq
call with multiple variable-value pairs. For conciseness, setq
can set any number of variables.
What happens if the user invokes unscroll
before any call to scroll-up
? The variables unscroll-point, unscroll-window-start
, and unscroll-hscroll
will all contain their default value, nil
. This value is unsuitable for passing to the functions goto-char, set-window-start
, and set-window-hscroll
. As soon as the call to goto-char
is reached, execution of the unscroll
command will abort with this error: “Wrong type argument: integer-or-marker-p, nil.” This means a function expecting an integer or a marker (to satisfy the predicate integer-or-marker-p
) was passed nil
instead. (Markers are explained in an earlier section of this chapter.)
To keep the user from being baffled by this cryptic error message, it’s a good idea to precede the call to goto-char
with a simple check and a more informative error message:
(if (not unscroll-point) ;i.e., if unscroll-point is nil (error "Cannot unscroll yet"))
When error
is invoked, execution of unscroll
aborts and the message “Cannot unscroll yet” is displayed.
It’s easy to press C-v when meaning to press C-b. That’s what led us to devise the unscroll
function. Now observe that it’s just as easy to press M-v (scroll-down
) when meaning to press M-b (backward-word
). It’s the same problem, but in the other direction, sort of. It would be nice if we could generalize unscroll
to undo scrolling in any direction.
The obvious way to generalize unscroll
is to advise scroll-down
in the same way that we advised scroll-up
:
(defadvice scroll-down (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (if (not (eq last-command 'scroll-down)) (setq unscroll-point (point) unscroll-window-start (window-start) unscroll-hscroll (window-hscroll))))
(Note that two functions, such as scroll-up
and scroll-down
, may have identically named pieces of advice, such as remember-for-unscroll
, without conflict.)
Now we must decide how we want unscroll
to behave in the case where we mingle erroneous C-vs with erroneous M-vs. In other words, suppose you mistakenly press C-v C-v M-v. Should unscroll
revert to the position before the M-v, or should it revert all the way back to the position before the first C-v?
I prefer the latter behavior. But this means that in the advice for scroll-up
, where we now test whether the last command was scroll-up
, we must now test whether it was either scroll-up
or scroll-down
, and do the same in scroll-down
.
(defadvice scroll-up (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (if (not (or (eq last-command 'scroll-up) (eq last-command 'scroll-down))) (setq unscroll-point (point) unscroll-window-start (window-start) unscroll-hscroll (window-hscroll)))) (defadvice scroll-down (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (if (not (or (eq last-command 'scroll-up) (eq last-command 'scroll-down))) (setq unscroll-point (point) unscroll-window-start (window-start) unscroll-hscroll (window-hscroll))))
Take a moment to make sure you understand the expression
(if (not (or (eq last-command 'scroll-up) (eq last-command 'scroll-down))) (setq ...))
It’s best to read such expressions by moving inward one level of subexpression at a time. Start with
(if (not ...) (setq ...))
“If something’s not true, set some variable(s).” Next, peer a little deeper:
(if (not (or ...)) (setq ...))
“If none of a set of conditions is true, set some variable(s).” Finally,
(if (not (or (eq last-command 'scroll-up) (eq last-command 'scroll-down))) (setq ...))
means, “If neither 'last-command
is scroll-up
' nor 'last-command
is scroll-down
' is true, set some variable(s).”
Suppose somewhere down the line, you come up with more commands you’d like to advise this way; let’s say scroll-left
and scroll-right
:
(defadvice scroll-up (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (if (not (or (eq last-command 'scroll-up) (eq last-command 'scroll-down) (eq last-command 'scroll-left) ;new (eq last-command 'scroll-right))) ;new (setq unscroll-point (point) unscroll-window-start (window-start) unscroll-hscroll (window-hscroll)))) (defadvice scroll-down (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (if (not (or (eq last-command 'scroll-up) (eq last-command 'scroll-down) (eq last-command 'scroll-left) ;new (eq last-command 'scroll-right))) ;new (setq unscroll-point (point) unscroll-window-start (window-start) unscroll-hscroll (window-hscroll)))) (defadvice scroll-left (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (if (not (or (eq last-command 'scroll-up) (eq last-command 'scroll-down) (eq last-command 'scroll-left) (eq last-command 'scroll-right))) (setq unscroll-point (point) unscroll-window-start (window-start) unscroll-hscroll (window-hscroll)))) (defadvice scroll-right (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (if (not (or (eq last-command 'scroll-up) (eq last-command 'scroll-down) (eq last-command 'scroll-left) (eq last-command 'scroll-right))) (setq unscroll-point (point) unscroll-window-start (window-start) unscroll-hscroll (window-hscroll))))
Not only is this very repetitive and error-prone, but for each new command that we wish to make “unscrollable,” the advice for each existing unscrollable command must have its last-command
test modified to include the new one.
Two things can be done to improve this situation. First, since the advice is identical in each case, it can be factored out into a shared function:
(defun unscroll-maybe-remember () (if (not (or (eq last-command 'scroll-up) (eq last-command 'scroll-down) (eq last-command 'scroll-left) (eq last-command 'scroll-right))) (setq unscroll-point (point) unscroll-window-start (window-start) unscroll-hscroll (window-hscroll)))) (defadvice scroll-up (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (unscroll-maybe-remember)) (defadvice scroll-down (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (unscroll-maybe-remember)) (defadvice scroll-left (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (unscroll-maybe-remember)) (defadvice scroll-right (before remember-for-unscroll activate compile) "Remember where we started from, for 'unscroll'." (unscroll-maybe-remember))
Second, instead of having to test for n possible values of last-command
, all meaning “the last command was unscrollable,” it would be nice if there were a single such value, and if all the unscrollable commands could somehow set last-command
to that value.
Enter this-command
, the variable that contains the name of the current command invoked by the user. In fact, the way last-command
gets set is this: while Emacs is executing a command, this-command
contains the name of the command; then when it is finished, Emacs puts the value of this-command
into last-command
.
While a command is executing, it can change the value of this-command
. When the next command runs, the value will be available in last-command
.
Let’s choose a symbol to represent all unscrollable commands: say, unscrollable
. Now we can change unscroll-maybe-remember
as follows:
(defun unscroll-maybe-remember () (setq this-command 'unscrollable) (if (not (eq last-command 'unscrollable)) (setq unscroll-point (point) unscroll-window-start (window-start) unscroll-hscroll (window-hscroll))))
Any command that calls unscroll-maybe-remember
now causes this-command
to contain unscrollable
. And instead of checking last-command
for four different values (more if we add new unscrollable commands), we only need to check for one value (even if we define new unscrollable commands).
Our improved unscroll-maybe-remember
works great, but (as perhaps you’ve come to expect by now) there are still some refinements we can make. The first is to address this problem: the variables this-command
and last-command
aren’t exclusively ours to do with as we please. They’re central to the Emacs Lisp interpreter, and other components of Emacs depend on them, too. For all we know, there exists an Emacs component that depends on the various scroll functions not overriding the settings of this-command
and last-command
. Still, we would like a single, distinguished value in last-command
to identify all unscrollable commands.
Here’s where symbol properties come in handy. In addition to having a variable value and/or a function definition, every Emacs Lisp symbol may also have associated with it a property list. A property list is a mapping from names to values. Each name is yet another Lisp symbol, while each value may be any Lisp expression.
Properties are stored with the put
function and retrieved with the get
function. Thus, if we give the value 17
to the property named some-property
belonging to the symbol a-symbol
:
(put 'a-symbol 'some-property 17)
then
(get 'a-symbol 'some-property)
returns 17
. If we try to get
a property from a symbol that doesn’t have that property, the result is nil
.
Instead of using unscrollable
as a value for this-command
and last-command
, we can instead use an unscrollable
property. We’ll set it up so that commands that are unscrollable have the unscrollable
property of their names set to t
, like so:
(put 'scroll-up 'unscrollable t) (put 'scroll-down 'unscrollable t) (put 'scroll-left 'unscrollable t) (put 'scroll-right 'unscrollable t)
This only has to be done once, before any calls to unscroll-maybe-remember
.
Now (get
x 'unscrollable)
will be true only when x is one of the symbols scroll-up, scroll-down, scroll-left
, and scroll-right
. For all other symbols, since the unscrollable
property is (presumably) undefined, the result will be nil
.
We can now change
(if (not (eq last-command 'unscrollable)) ...)
in unscroll-maybe-remember
to
(if (not (get last-command 'unscrollable)) ...)
and we can also stop assigning unscrollable
to this-command
:
(defun unscroll-maybe-remember () (if (not (get last-command 'unscrollable)) (setq unscroll-point (point) unscroll-window-start (window-start) unscroll-hscroll (window-hscroll))))
How can we make this code even better? Suppose you inadvertently scroll-down
a few times and you want to unscroll
. But before you do, you happen to see a bit of text you’d like to change, and you change it. Then you unscroll
. The screen hasn’t been correctly restored!
The reason is that editing text earlier in the buffer changes all the subsequent buffer positions. An edit involving a net addition or removal of n characters adds or subtracts n to or from all subsequent positions. Therefore the saved buffer positions in the variables unscroll-point
and unscroll-window-start
will be off by n. (If n is zero, you got lucky.)
Instead of using absolute positions as the values of unscroll-point
and unscroll-window-start
, it would be a good idea to use markers. A marker is a special object that specifies a buffer position just like an integer does. But if the buffer position moves because of insertions or deletions, the marker “moves” too so that it keeps pointing to the same spot in the text.
Since we’re changing unscroll-point
and unscroll-window-start
to be markers, we no longer initialize them with nil
. We instead initialize them as new, empty marker objects using the function make-marker
:
(defvar unscroll-point (make-marker) "Cursor position for next call to 'unscroll'.") (defvar unscroll-window-start (make-marker) "Window start for next call to 'unscroll'.")
The function set-marker
is used to set the position of a marker.
(defun unscroll-maybe-remember () (if (not (get last-command 'unscrollable)) (progn (set-marker unscroll-point (point)) (set-marker unscroll-window-start (window-start)) (setq unscroll-hscroll (window-hscroll)))))
The call to progn
is back because the single call to setq
has been split up into several function calls. We don’t use a marker for unscroll-hscroll
because its value isn’t a buffer position.
We don’t need to rewrite unscroll
, because goto-char
and set-window-start
can both handle arguments that are markers as well as arguments that are integers. So the previous definition (reprinted here for convenience) will continue to work:
(defun unscroll () "Revert to 'unscroll-point' and 'unscroll-window-start'." (interactive) (goto-char unscroll-point) (set-window-start nil unscroll-window-start) (set-window-hscroll nil unscroll-hscroll))
When we declare unscroll-point
and unscroll-marker
, we create “empty” marker objects and reuse them in each call to unscroll-remember
, rather than creating new marker objects in each call to unscroll-remember
and discarding the old objects. This is an optimization. Not only is it better, in general, to avoid very prolific object creation when possible, but markers happen to be more expensive than other objects to create. Each marker that points into some buffer somewhere has to be updated every time text is inserted or deleted in that buffer. A discarded marker object will eventually be reclaimed by the garbage collector, but until it is, it’ll slow down editing in its buffer.
In general, when you intend to discard a marker object m (meaning that you no longer intend to refer to its value), it’s a good idea to first make it point “nowhere” by doing this:
(set-marker m nil)
[16] Although in Chapter 2, we used defalias
to make scroll-ahead
and scroll-behind
synonyms for scroll-up
and scroll-down
, in this chapter we’ll refer to scroll-up
and scroll-down
by their original names.
[17] If you think the way not
works sounds like the way null
works, you’re right—they’re exactly the same function. One is simply an alias for the other. Which one you use is a readability issue. Use null
when testing to see whether an object is the empty list. Use not
when inverting truth values.