Some Emacs advice
First, A Note
Last year, I experimented with making some videos about Emacs. I really enjoyed the process and the very nice feedback I received, but – obviously – I haven’t made one in a while.
There are a few reasons for that. The first is that I’ve been quite busy – I moved at the end of the year and work on various projects has kept me quite occupied. That’s more of an excuse though.
The bigger reason is that I realized how much YouTube as a platform made me unhappy.
I drastically cut back on my consumption, now only watching a few channels via RSS & youtube-dl
(in Emacs via elfeed
, of course).
It felt disingenuous to me to then continue to create content for others on a platform that I don’t want to use.
I’ve considered looking in to putting videos on Peertube or something like that, but that would certainly be more effort, which brings me to the third reason: I personally don’t really like consuming content, particularly technical content, by video. I much prefer to read things and it’s hard to motivate myself to make content that I wouldn’t really want to consume myself.
Hopefully that makes sense; now, on to the fun part.
Advising Emacs
As I’ve mentioned in some of my previous Emacs posts & videos, there are many ways to customize the behaviour of existing packages in Emacs.
Sometimes you’re lucky enough that the thing you want to change is just an option and you can simply set a variable – or even, if the setting was defined with defcustom
, you can use M-x customize
to use a nice little GUI to change the setting.
If that’s not an option, or doesn’t fit what you want to do, “hooks” are next level of customization.
The idea of hooks is that you can register functions to be called at a particular time.
In the library, this is as simple as defining a variable ((defvar my-hook nil)
or (defcustom my-hook nil :type 'hook)
) and then running it at an appropriate time with (run-hook 'my-hook)
.
Hooks are very powerful, since you can run arbitrary code, but you’re limited to the hooks that the library author has put in place – if there’s another time or place you want to run some of your own code and there isn’t already a hook there, you’re out of luck.
Enter “advice” – a way to have some of your own code run whenever a particular function gets invoked.
History
The concept of advice, as far as I know, comes from the Common Lisp Object System (CLOS) and was briefly popular in the Java world under the term “Aspect Oriented Programming”. It is a mechanism for not just overriding, but adding some additional functionality before, after, or around an existing function. In Emacs terms, you could almost think of it as every existing function not only having a “before” and “after” hook, but with functions in those hooks being able to change the arguments or return value, or even not call the original function.
There are two different ways of using advice in Emacs – defadvice
, from the original advice.el
(written in 1993) and the newer advice-add
from nadvice.el
(added in 2012).
The newer library is much simpler in both functionality and implementation (advice.el
is 3262 lines long, while nadvice.el
is less than a fifth as long, at a compact 592 lines) and is generally recommended unless you need some of the advanced features of defadvice
.
Using Advice
We’ll take a quick like at the older defadvice
first; just the most basic features, since it has a lot of flexibility, but I never use it.
The basic usage looks like:
(defadvice function-to-be-advised (type-of-advice name-of-this-advice) ;; code )
Where type-of-advice
is before
, after
, or around
.
There are many other options and ways of using defadvice
that you can learn about by reading the documentation.
For example, let’s say you want to make it easier to create new entries in org-mode lists, so you want to be able to hit M-RET
anywhere in the current line and have it create a new entry below, instead of splitting the current entry.
One way to do this with advice would be as follows:
(defadvice org-meta-return (before eol-before) (org-end-of-line)) (ad-activate 'org-meta-return) ;; or, equivalently (defadvice org-meta-return (before eol-before-2 activate) (org-end-of-line))
Here’s the equivalent using advice-add
, something I actually have in my emacs configuration:
(advice-add 'org-meta-return :before #'org-end-of-line)
advice-add
is a bit simpler and has fewer options.
It has some optional arguments, but you mostly just need the three required ones here: A symbol naming the function to advise, the type of advice, and the advising function.
There are actualy ten different types of advice you can use (see the help for add-function
for a list and an explanation of what they all are), but in practice one mainly just uses four: :before
, :after
, :override
and :around
.
:before
and :after
are pretty straight-forward – they get called either before or after the original function is called and will recieve the same arguments as the original function.
:override
simply replaces the original function with the advising one.
:around
is more flexible – it recieves as argument the original function itself and all the arguments it was called with, so you can essentially do whatever you want: Do things before, then call the function but change some arguments, modify the return value, and do something after.
Sky’s the limit!
The other options are essentially convenience wrappers to make some simple patterns of :around
advice simpler to implement.
The inverse of advice-add
is advice-remove
.
You can either use it to remove some advice you’ve added to a function, making it a much cleaner process if you want to only temporarily or conditionally change a function’s definition.
Examples of Use
Finally, let’s have a look at some practical uses of advice.
I have a couple uses of advice in my Emacs config.
All of them are using the new advice-add
– I haven’t had anything arise thus far that would require defadvice
.
Tracking the Active Window
;; variable to keep track of window (defvar cogent-line-selected-window (frame-selected-window)) ;; set current (defun cogent-line-set-selected-window (&rest _args) (when (not (minibuffer-window-active-p (frame-selected-window))) (setq cogent-line-selected-window (frame-selected-window)) (force-mode-line-update))) ;; unset current (defun cogent-line-unset-selected-window () (setq cogent-line-selected-window nil) (force-mode-line-update)) ;; use hooks when we can (add-hook 'window-configuration-change-hook #'cogent-line-set-selected-window) (add-hook 'focus-in-hook #'cogent-line-set-selected-window) (add-hook 'focus-out-hook #'cogent-line-unset-selected-window) ;; when no hooks exist, advice! (advice-add 'handle-switch-frame :after #'cogent-line-set-selected-window) (advice-add 'select-window :after #'cogent-line-set-selected-window)
Here’s a combination of using hooks when we can and advice when no appropriate hook exists. This bit of functionality was taken from the modeline library from Spacemacs; it keeps track of which window has been selected, which is then used to render the modeline different in the focused window versus the others.
In this case, we use the simple :after
advice to just run our function, cogent-line-set-selected-window
after handle-switch-frame
and select-window
are called.
Making LSP “Peek” in a Posframe
via https://github.com/emacs-lsp/lsp-ui/issues/441:
(defun lsp-ui-peek--peek-display (src1 src2) (-let* ((win-width (frame-width)) (lsp-ui-peek-list-width (/ (frame-width) 2)) (string (-some--> (-zip-fill "" src1 src2) (--map (lsp-ui-peek--adjust win-width it) it) (-map-indexed 'lsp-ui-peek--make-line it) (-concat it (lsp-ui-peek--make-footer))))) (setq lsp-ui-peek--buffer (get-buffer-create "*lsp-peek--buffer*")) (posframe-show lsp-ui-peek--buffer :string (mapconcat 'identity string "") :min-width (frame-width) :poshandler #'posframe-poshandler-frame-center))) (defun lsp-ui-peek--peek-destroy () (when (and (boundp 'lsp-ui-peek--buffer) (bufferp lsp-ui-peek--buffer)) (posframe-delete lsp-ui-peek--buffer)) (setq lsp-ui-peek--buffer nil lsp-ui-peek--last-xref nil) (set-window-start (get-buffer-window) lsp-ui-peek--win-start)) (advice-add #'lsp-ui-peek--peek-new :override #'lsp-ui-peek--peek-display) (advice-add #'lsp-ui-peek--peek-hide :override #'lsp-ui-peek--peek-destroy))
This uses advice to override the existing lsp-ui-peek--display
and lsp-ui-peek--destroy
functions to show in a posframe instead of the normal way they do.
Pretty straight-forward; unlike just re-defining the functions though, using advise means we could also use advice-remove
to restore the original behaviour without any shenanigans.
Making My Own Little Theming System
(defvar cogent/theme-hooks nil "((theme-id . function) ...)") (defun cogent/load-theme-advice (f theme-id &optional no-confirm no-enable &rest args) ; "Enhance `load-theme' by disabling other enabled themes & calling hooks" (unless no-enable ; (mapc #'disable-theme custom-enabled-themes)) (prog1 (apply f theme-id no-confirm no-enable args) ; (unless no-enable ; (pcase (assq theme-id cogent/theme-hooks) (`(,_ . ,f) (funcall f)))))) (advice-add 'load-theme :around #'cogent/load-theme-advice) ;
- advice-add is installing our advising function –
cogent/load-theme-advice
– “around” theload-theme
function. Because we specifiy:around
, our function will receive original function as an argument, plus all the arguments it receives, and it will be our responsibility to invoke it - cogent/load-theme-advice is the function that is getting called in place of the advised function,
load-theme
- (unless …) some code we’re running before we call the original function; in this case, disabling the existing themes (by default,
load-theme
is additive, which can be somewhat confusing) - (apply f …) is invoking the advised function. In this case, we’re just passing in the same arguments that we received, but we have the flexibility to do whatever we want with it.
- (unless … (pcase …)) after we run the original function, we run this code to run my own “hook” functions for the particular theme that’s been loaded