Posted on

Indent on Save, Maybe

While working on a .NET project, I have noticed that when I do many things, Visual Studio takes that time to re-indent large amounts of my code. But most of the time I’m writing it out in Emacs, so this makes my diffs a pain in the ass. But it got me to thinking about indenting on save. And then—lo—the very subject came up today on the Emacs mailing list. A teacher was asking for a way to make C++ mode automatically re-indent the whole file on save, to make his students use. I don’t know if he was sick of seeing horrible indentation or what.

There is a hook in Emacs called the before-save-hook, to which you can add functions to be called just before saving the contents of a buffer. A great example:

(add-hook 'before-save-hook 'delete-trailing-whitespace)

Will call delete-trailing-whitespace whenever you save any file.

So with that hook in mind, here was my first idea, which I sent back to the guy.

(add-hook 'before-save-hook
          (lambda ()
            (when (eq 'c++-mode
                      (buffer-local-value 'major-mode (current-buffer)))
              (indent-region (point-min) (point-max)))))

Each buffer has a local variable called major-mode that names the current major mode—appropriately enough. This anonymous function checks to see if that mode is c++-mode, and if it does then it calls indent-region with two arguments: the start and end points of the region we want to indent. The functions point-min and point-max return the minimum and maximum points of the buffer. So this indents the whole thing.

But a more experienced Emacs Lisp developer on the mailing list pointed out to me a better way. His code:

(defun c++-indent-buffer-maybe ()
  (when (and (string-match
              "\\(foo\\|bar\\)\\.cpp\\'"
              (buffer-file-name))
             (y-or-n-p "Indent buffer before saving ?"))
    (indent-region (point-min)
                   (point-max))))

(defun my-c++-hook ()
  (add-hook 'before-save-hook 'c++-indent-buffer-maybe nil t))

(add-hook 'c++-mode-hook 'my-c++-hook)

His code looks at the filename of the buffer to decide whether or not the file should be indented. I prefer checking the major mode still, but the teacher who asked the original question also wanted to see a filename-based solution.

This version also prompts—via the function y-or-n-p—to make sure the user wants to indent the whole buffer. If the user types ‘n’, the buffer is still saved, but not indented. This is useful, but adds an extra key-press on every save. Personally, I think I would go without it.

But the best way in which this code is better is how it sets the hook as a mode hook for C++ mode. One problem with using before-save-hook is it is called for every buffer. So my original code would be checking every time I save anything. Which is overkill. The function my-c++-hook above adds the indenting function to before-save-hook, but sets it as a buffer-local hook. This is done by the last argument. If we look at the definition of add-hook:

(add-hook HOOK FUNCTION &optional APPEND LOCAL)

The first optional argument APPEND decides where in the hook list our function goes. The LOCAL argument, if true, makes the hook local to the current buffer.

So via a little indirection, the code above makes sure that the indented code is only run when saving C++ buffers, instead of checking it when saved everywhere.

A useful combination of these would be:

(defun c++-indent-buffer-maybe ()
  (if (y-or-n-p "Indent buffer before saving?")
      (indent-region (point-min)
                     (point-max))))

(add-hook 'c++-mode-hook
          (lambda ()
            (add-hook 'before-save-hook 'c++-indent-buffer-maybe nil t)))

Not hard to imagine how to extend this to other modes.