Well, timestamps were marginally useful, and writestamps were somewhat more so, but modifystamps may be even better. A modifystamp is a writestamp that records the time the file was last modified, which may not be the same as the last time it was saved to disk. For instance, if you visit a file and save it under a new name without making any changes to it, you shouldn't cause the modifystamp to change.
Emacs has a hook variable called
first-change-hook. Whenever a buffer is changed for the first time since it was last saved, the functions in
first-change-hook get executed. Implementing modifystamps by using this hook merely entails moving our old
update-writestamps function from
first-change-hook. Of course, we'll also want to change its name to
update-modifystamps, and introduce new variables—
modifystamp-format, modifystamp-prefix, and
modifystamp-suffix—that work like their writestamp counterparts without overloading the writestamp variables. Then
update-modifystamps should be changed to use the new variables.
Before any of this happens,
first-change-hook, which is normally global, should be made buffer-local. If we add
first-change-hook while it is still global,
update-modifystamps will be called every time any buffer is saved. Making it buffer-local in the current buffer causes changes to the variable to be invisible outside that buffer. Other buffers continue to use the default global value.
Although ordinary variables are made buffer-local with either
make-variable-buffer-local (see below), hook variables must be made buffer-local with
(defvar modifystamp-format "%C" "*Format for modifystamps (c.f. 'format-time-string').") (defvar modifystamp-prefix "MODIFYSTAMP((" "*String identifying start of modifystamp.") (defvar modifystamp-suffix "))" "*String that terminates a modifystamp.") (defun update-modifystamps () "Find modifystamps and replace them with the current time." (save-excursion (save-restriction (save-match-data (widen) (goto-char (point-min)) (let ((regexp (concat "^" (regexp-quote modifystamp-prefix) "\\(.*\\)" (regexp-quote modifystamp-suffix) "$"))) (while (re-search-forward regexp nil t) (replace-match (format-time-string modifystamp-format (current-time)) t t nil 1)))))) nil) (add-hook 'first-change-hook 'update-modifystamps nil t)
nil argument to
add-hook is just a place holder. We care only about the last argument,
t, which means "change only the buffer-local copy of
The problem with this approach is that if you make ten changes to the file before saving it, the modifystamps will contain the time of the first change, not the last change. Close enough for some purposes, but we can do better.
This time we'll go back to using
local-write-file-hooks, but we'll call
update-modifystamps from it only if
buffer-modified-p returns true, which tells us that the current buffer has been modified since it was last saved:
(defun maybe-update-modifystamps () "Call 'update-modifystamps' if the buffer has been modified." (if (buffer-modified-p) (update-modifystamps))) (add-hook 'local-write-file-hooks 'maybe-update-modifystamps)
Now we have the opposite problem from simple approach #1: the last-modified time isn't computed until the file is saved, which may be much later than the actual time of the last modification. If you make a change to the file at 2:00 and save at 3:00, the modifystamps will record 3:00 as the last-modified time. This is a closer approximation, but it's still not perfect.
Theoretically, we could call
update-modifystamps after every change to the buffer, but in practice it's prohibitively expensive to scan through the whole file and rewrite parts of it after every keystroke. But it's not too expensive to memorize the current time after each buffer change. Then, when the buffer is saved to a file, the memorized time can be used for computing the time in the modifystamps.
The hook variable
after-change-functions contains functions to call after each buffer change. First let's make it buffer-local:
Now we define a buffer-local variable to hold this buffer's latest modification time:
(defvar last-change-time nil "Time of last buffer modification.") (make-variable-buffer-local 'last-change-time)
make-variable-buffer-local causes the named variable to have a separate, buffer-local value in every buffer. This is subtly different from
make-local-variable, which makes a variable have a buffer-local value in the current buffer while allowing other buffers to share the same global value. In this case, we use
make-variable-buffer-local because there is no meaningful global value of
last-change-time for other buffers to share.
(add-hook 'after-change-functions 'remember-change-time nil t)
after-change-functions are passed three arguments describing the change that just took place (see the section called Mode Meat in Chapter 7). But
remember-change-time doesn't care what the change was; only that there was a change. So we'll allow
remember-change-time to take arguments, but we'll ignore them.
(defun remember-change-time (&rest unused) "Store the current time in 'last-change-time'." (setq last-change-time (current-time)))
&rest, followed by a parameter name, must appear last in a function's parameter list. It means "collect up any remaining arguments into a list and assign it to the last parameter" (
unused in this case). The function may have other parameters, including
&optional ones, but these must precede the
&rest parameter. After all the other parameters are assigned in the normal fashion, the
&rest parameter gets a list of whatever's left. So if a function is defined as
(defun foo (a b &rest c) ...)
and is called with
(foo 1 2 3 4), then
a will be
1, b will be
c will be the list
In some situations,
&rest is very useful, even necessary; but right now we're only using it out of laziness (or economy, if you prefer), to avoid having to name three separate parameters that we don't plan to use.
Now we must revise
update-modifystamps: it must use the time stored in
last-change-time instead of using
(current-time). For efficiency, it should also reset
nil when it is done, so if the file is subsequently saved without being modified, we can avoid the overhead of calling
(defun update-modifystamps () "Find modifystamps and replace them with the saved time." (save-excursion (save-restriction (save-match-data (widen) (goto-char (point-min)) (let ((regexp (concat "^" (regexp-quote modifystamp-prefix) "\\(.*\\)" (regexp-quote modifystamp-suffix) "$"))) (while (re-search-forward regexp nil t) (replace-match (format-time-string modifystamp-format last-change-time) t t nil 1)))))) (setq last-change-time nil) nil)
Finally, we wish not to call
(defun maybe-update-modifystamps () "Call 'update-modifystamps' if the buffer has been modified." (if last-change-time ;instead of testing (buffer-modified-p) (update-modifystamps)))
There's still one important thing missing from
maybe-update-modifystamps. Before reading ahead to the next section, can you figure out what it is?
The problem is that every time a modifystamp gets rewritten by
update-modifystamps, the buffer changes, causing
last-change-time to change! Only the first modifystamp will be correctly rewritten. Subsequent ones will contain a time much closer to when the file was saved than when the last modification was made.
One way around this problem is to temporarily set the value of
nil while executing
update-modifystamps as shown below.
(add-hook 'local-write-file-hooks '(lambda () (if last-change-time (let ((after-change-functions nil)) (update-modifystamps)))))
This use of
let creates a temporary variable,
after-change-functions, that supersedes the global
after-change-functions during the call to
update-modifystamps in the body of the
let. After the
let exits, the temporary
after-change-functions disappears and the global one is again in effect.
This solution has a drawback: if there are other functions in
after-change-functions, they'll also be disabled during the call to
update-modifystamps, though you might not intend for them to be.
A better solution would be to "capture" the value of
last-change-time before any modifystamps are updated. That way, when updating the first modifystamp causes
last-change-time to change, the new value of
last-change-time won't affect any remaining modifystamps because
update-modifystamps won't be referring to
The simplest way to "capture" the value of
last-change-time is to pass it as an argument to
(add-hook 'local-write-file-hooks '(lambda () (if last-change-time (update-modifystamps last-change-time))))
This requires changing
update-modifystamps to take one argument and use it in the call to
(defun update-modifystamps (time) "Find modifystamps and replace them with the given time." (save-excursion (save-restriction (save-match-data (widen) (goto-char (point-min)) (let ((regexp (concat "^" (regexp-quote modifystamp-prefix) "\\(.*\\)" (regexp-quote modifystamp-suffix) "$"))) (while (re-search-forward regexp nil t) (replace-match (format-time-string modifystamp-format time) t t nil 1)))))) (setq last-change-time nil) nil)
You might be thinking that setting up a buffer to use modifystamps involves evaluating a lot of expressions and setting up a lot of variables, and that it seems hard to keep track of what's needed to make modifystamps work. If so, you're right. So in the next chapter, we'll look at how you can encapsulate a collection of related functions and variables in a Lisp file.