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-column
to 80, enable fci-mode
(which has a marker which
indicates when you are over the fill-column
), enables
electric-pair-mode
(which inserts closing parens or similar item
when you type the open parens), and sets the whitespace-style
so
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-local
was 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-p
returns true if the variable has been set
and is now local.
fill-column
is a variable that will become local whenever it is set,
as the documentation mentions. So this is safe to set. However
whitespace-style
does 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-variable
returns the
unquoted function, and therefore it can be used with set
.
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.
No comments:
Post a Comment