Wednesday, November 30, 2011

How to Make Emacs' Scratch Buffer Persistent Across Sessions

A Quick Elisp Tutorial

Emacs has a handy, but sometimes decried, feature called the "scratch" buffer. This is a special buffer which is created upon startup and allows the user to type in and evaluate Emacs Lisp code. Handy for editing tasks too specific (or not useful enough) to put into an function and handy for exploratory Emacs Lisp interactive development (although this development is just as easily accomplished in any file in Lisp mode).

One problem with *scratch* is that its tempting to put significant bits of code (and other information) into it. This isn't a problem in itself, but *scratch* isn't associated with a file, and its contents are lost without warning when Emacs is closed. Today we'll modify the default behavior of Emacs so that it saves the scratch buffer to a file on exit and loads it back in on startup.

This will serve as a brief Elisp tutorial.

Get it?

Get it?

Preliminaries

If you are totally new to Emacs, I'd just put the code we are about to write directly in your .emacs or .emacs.d/init.el file. These files are executed when emacs starts, and since we need to ensure that our behavior happens on startup, this is a reasonable place to put code. After years of emacs usage, your .emacs will balloon into an unspeakable cthonian mess, and you'll have to refactor it, but by then you'll know what you need to for that process.

Let's start by declaring a few parameters:

(defvar persistent-scratch-filename 
    "~/.emacs-persistent-scratch"
    "Location of *scratch* file contents for persistent-scratch.")
(defvar persistent-scratch-backup-directory 
    "~/.emacs-persistent-scratch-backups/"
    "Location of backups of the *scratch* buffer contents for
    persistent-scratch.")

defvar declares variables, associates them with a value (the second term), and a doc-string (the third term). Providing a doc-string will ensure that when these symbols appear in the result of an apropos search, they will have a convenient description associated with them. It also will help us out when we look at the code in 4 years. Don't forget to create these directories before trying the code! (Or, modify the code to check for their existence and create them otherwise!)

Our code is going to backup any version of the scratch contents before overwriting them, just in case something really important was hiding in there. That means we need to copy the current scratch file contents (not the contents of the scratch buffer, but the last version of it) to a backup file. That file needs to have a generated name. We'll use the date to disambiguate scratch backups:

(defun make-persistent-scratch-backup-name ()
  "Create a filename to backup the current scratch file by
  concatenating PERSISTENT-SCRATCH-BACKUP-DIRECTORY with the
  current date and time."
  (concat 
   persistent-scratch-backup-directory 
   (replace-regexp-in-string 
     (regexp-quote " ") "-" (current-time-string))))

Again, the initial string in the body of this function is documentation. Emacs will show us this description when it displays information about this function. concat concatenates strings, current-time-string does what you'd think, and we use replace-regexp-in-string to remove the spaces. regexp-quote produces a regular expression which matches exactly its input string and nothing else.

Saving the Contents of *scratch*

Next is our function to save the contents of the *scratch* buffer. We will eventually place this function on a "hook" which ensures it gets run every time emacs shuts down.

(defun save-persistent-scratch ()
  "Write the contents of *scratch* to the file name
  PERSISTENT-SCRATCH-FILENAME, making a backup copy in
  PERSISTENT-SCRATCH-BACKUP-DIRECTORY."
  (with-current-buffer (get-buffer "*scratch*")
    (if (file-exists-p persistent-scratch-filename)
        (copy-file persistent-scratch-filename
                   (make-persistent-scratch-backup-name)))
    (write-region (point-min) (point-max) 
                  persistent-scratch-filename)))

This function needs to work with the contents of the *scratch* buffer, so it uses the special form with-current-buffer to create a context where actions refer to the contents of that buffer.

We then check to see whether a file containing the previous scratch buffer is present, and if it is we use copy-file to copy it to the backup directory. We then use write-region to write the contents of *scratch* from (point-min) to (point-max) (that is, the whole thing), to the scratch file location.

Loading the Contents of *scratch*

(defun load-persistent-scratch ()
  "Load the contents of PERSISTENT-SCRATCH-FILENAME into the
  scratch buffer, clearing its contents first."
  (if (file-exists-p persistent-scratch-filename)
      (with-current-buffer (get-buffer "*scratch*")
        (delete-region (point-min) (point-max))
        (shell-command (format "cat %s" persistent-scratch-filename) (current-buffer)))))

(It has been correctly pointed out in the comments that insert-file is the more idiomatic way of getting file contents into a buffer, which I somehow forgot about!) We first check to see whether there is a file containing the previous session's scratch contents. If there is, we switch to the scratch buffer context with with-current-buffer and use shell-command and cat to insert the contents into *scratch*.

Hooking It All In

Since we are writing this in our .emacs file, we can ensure that the contents of the last session's *scratch* are read in by simply writing:

(load-persistent-scratch)

This will get executed on emacs startup, just as we want. We have to do something more complicated to ensure that save-persistent-scratch is run whenever emacs exits.

To do this, we use one of emacs's many "hooks". A hook, in emacs parlance, is a list of functions which emacs promises to run at specific times or when specific events occur. The hook we need is kill-emacs-hook. This hook is run whenever emacs is killed for any reason.

We can use the special form push, which pushes an item onto the start of a list, to add our function.

(push #'save-persistent-scratch kill-emacs-hook)

Note that save-persistent-scratch is preceded by a #'. This tells emacs that we want the function associated with the symbol save-persistent-scratch, not the value (of which there is none anyway).

That is It!

That's it. Save this into your .emacs and you should now have a scratch buffer which remembers its contents between sessions. Handy!


7 comments:

siancu said...

Hello,

On Emacs trunk I'm getting this error when exiting Emacs:

Debugger entered--Lisp error: (file-error "Opening output file" "No such file or directory" "/Users/my_user/Dropbox/.emacs-persistent-scratch-backups/Thu-Dec--1-10:04:02-2011")
copy-file("~/Dropbox/.emacs-persistent-scratch" "~/Dropbox/.emacs-persistent-scratch-backups/Thu-Dec--1-10:04:02-2011")
(if (file-exists-p persistent-scratch-filename) (copy-file persistent-scratch-filename (make-persistent-scratch-backup-name)))
(save-current-buffer (set-buffer (get-buffer "*scratch*")) (if (file-exists-p persistent-scratch-filename) (copy-file persistent-scratch-filename (make-persistent-scratch-backup-name))) (write-region (point-min) (point-max) persistent-scratch-filename))
(with-current-buffer (get-buffer "*scratch*") (if (file-exists-p persistent-scratch-filename) (copy-file persistent-scratch-filename (make-persistent-scratch-backup-name))) (write-region (point-min) (point-max) persistent-scratch-filename))
save-persistent-scratch()
kill-emacs()
save-buffers-kill-emacs(nil)
save-buffers-kill-terminal(nil)
call-interactively(save-buffers-kill-terminal nil nil)

Any ideas why?

Thanks!

S.

J.V. Toups said...

Siancu,

You probably need to create the directory in question before you run the extension for the first time. You can do that directly in elisp with the `make-directory` function.

I leave it as an exercise to the reader to extend this extension so that it checks for the existence of the directory, and makes it, on startup.

imperix said...

Usefull!
But 2 things to improve:
1. windows forbids filenames with ":", so this solution would be better: (format-time-string "%d%m%y_%H%M%S") instead of (current-time-string) which returns a time-string like 01:21:12.
2. (insert-file persistent-scratch-filename) instead of (shell-command (format "cat %s" persistent-scratch-filename) (current-buffer))
is much easier and also windows-compatible.

imperix said...
This comment has been removed by the author.
J.V. Toups said...

Excellent points!

Emacs said...

I think the time string in persistent name should be in good format, such as year-month-day_hour-min-sec, here is my version:

(defun make-persistent-scratch-backup-name ()
"Create a filename to backup the current scratch file by
concatenating PERSISTENT-SCRATCH-BACKUP-DIRECTORY with the
current date and time."
(concat
persistent-scratch-backup-directory
(format-time-string "%y-%m-%d_%H-%M-%S_%s" (current-time)))))

Phil Hudson said...

To be precise, `insert-file' is for interactive use only. Use `insert-file-contents'.