hooks. These are variables that hold a list of functions to call when they are triggered. Most modes have a hook for when they are enabled, which lets you then use the hook to customize based on modes. For example, here's how I currently customize c++ and Java modes:
(add-hook 'c++-mode-hook (lambda () (setq fill-column 80) (fci-mode 1) (electric-pair-mode) ;; compatible with fci-mode (setq whitespace-style '(face trailing)))) (add-hook 'java-mode-hook (lambda () (setq fill-column 80) (fci-mode 1) (electric-pair-mode)))
This means that when I load or create a c++ file, we'll set the
fill-columnto 80, enable
fci-mode(which has a marker which indicates when you are over the
electric-pair-mode(which inserts closing parens or similar item when you type the open parens), and sets the
whitespace-styleso that trailing whitespace is visible.
To use hooks properly, you have to understand and global and local variables. In elisp, all variables are global. But there are different kinds of global variables. One is the truly global variable. If you set it in one buffer, it is set for all buffers. Some variables are local, so they are the same in all buffers except the one in which
make-variable-localwas called. Finally, there's a variable that is always local to a buffer whenever it is a setup with
make-variable-buffer-local. There's also a similar
make-variable-frame-local. To set a variable in a hook, you are setting it in the context of a buffer. You want to ensure when you set it, it is set just in that buffer. So it needs to be either buffer-local, or you need to make it local. The easiest way to check to see if a variable is local or buffer-local is to look at it's help page
C-h f. If the variable is local, it will be noted there. You can programmatically do this with
local-variable-if-set-p, which will return a true value if the variable will be local when set. The similarly
local-variable-preturns true if the variable has been set and is now local.
fill-columnis a variable that will become local whenever it is set, as the documentation mentions. So this is safe to set. However
whitespace-styledoes not have documentation that mentions this. And if we check manually in
ielm, we can confirm this:
ELISP> (local-variable-if-set-p 'fill-column) t ELISP> (local-variable-if-set-p 'whitespace-style) nil
This looks like a bug in my customization code, then. Whenever I'm loading a c++ file, I'm changing the global value of
whitespace-style!We can fix this by making the variable buffer local before we set it.
(add-hook 'c++-mode-hook (lambda () (setq fill-column 80) (fci-mode 1) (electric-pair-mode) ;; compatible with fci-mode (set (make-local-variable 'whitespace-style) '(face trailing))))
This uses the fact that the function
make-local-variablereturns the unquoted function, and therefore it can be used with
We can improve this further. If you look at the C++ and Java versions, you can see that there is considerable overlap. It's useful to make everything a function, so that if there's any issue, we can simply redefine it to change the behavior. Otherwise, if you just used a lambda, and there's an issue with it, you'd have to remove the hook manually and re-add it after fixing.
Here's our factored code:
(defun ash/c-like-initialization ()
(setq fill-column 80) (fci-mode) (electric-pair-mode)) (defun ash/show-trailing-whitespace () (set (make-local-variable 'whitespace-style) '(face trailing))) (add-hook 'c++-mode-hook 'ash/c-like-initialization) (add-hook 'c++-mode-hook 'ash/show-trailing-whitespace) (add-hook 'java-mode-hook 'ash/c-like-initialization)
This is much cleaner, and now it's much easier to add new behaviors to either C++ or Java mode.
You may notice the code above has functions that start with
ash/, such as
ash/show-trailing-whitespace. Elisp allows all sorts of characters in identifiers, including slashes. It's wise to use a personal prefix in your elisp, so that nothing you do conflicts with built-in functions or packages you may have loaded. I used to use the prefix with a dash, but I've recently seen many uses of the slash, and agree it's better. The slash makes the namespacing clear.
With the refactorings in mind, here are the key ideas to remember when working with your own configuration file:
- When setting variables in a hook, make sure each variable is a local variable. If not, make it a local variable in the code you are adding to the hook.
- Prefer functions to lambdas in hooks.
- Use a namespace, separated with a slash, for your named functions and variables.