nullprogram.com/blog/2013/01/22/
Today at work I was using impatient-mode to share some code
with Brian. It makes for a really handy live pastebin. To
limit the buffer to the relevant code, I narrowed it down with
narrow-to-region
. However, the browser wouldn’t update to show only
the narrowed region until I made an edit. This makes sense because
impatient-mode hooks after-change-functions
. Narrowing the buffer
doesn’t change anything in the buffer, so, as expected, this hook is
not called.
The solution would be to also join whatever hook is called when the
buffer restriction changes. Unfortunately,
no such hook exists. I thought I could create this hook with
some advice, but this turns out to be currently impossible.
Emacs Advice
What’s advice? It’s a handy feature of Emacs lisp that allows users to
modify the behavior of almost any function without having to redefine
it. It works a little bit like methods in the Common Lisp Object
System (CLOS): advice is code than can be evaluated before, after, or
around a function.
Advice is defined with defadvice
. Duh. For example, say we wanted to
be silly and have Emacs say “Ouch!” when a line is killed with
kill-line
. We can advise this function to display a message.
(defadvice kill-line (after say-ouch activate)
(message "Ouch!"))
This says we want to advise the function kill-line
, we want this
advise to execute after kill-line
has run, our advice is named
“say-ouch
”, and we want to immediately activate this advice so it
gets used right away. The rest is the body of the advice, like the
body of a function. After evaluating this defadvice
, every time I
hit C-k
Emacs says “Ouch!” in the minibuffer. Cool!
narrow-to-region and widen
A hook is a variable that holds a list of functions. (Or maybe hooks
are the functions in this list? Emacs’ documentation calls both of
these things hooks.) These functions are called, usually without
arguments, when some specific event occurs. For example, every mode
has its own mode hook which is called when the mode is activated in a
buffer. This allows users to extend or modify the mode — like by
enabling additional minor modes — without editing the mode’s source
code directly.
To make our hook work we need to advise narrow-to-region
and widen
to run the hook after they’ve done their work. These are the primitive
narrowing functions which all the other narrowing functions eventually
call, like narrow-to-defun
, narrow-to-page
, and any other
mode-specific narrowing. Advising these two functions will cover all
buffer narrowing. It should be this simple.
(defvar change-restriction-hook ())
(defadvice narrow-to-region (after hook activate)
(run-hooks 'change-restriction-hook))
(defadvice widen (after hook activate)
(run-hooks 'change-restriction-hook))
At first this seems to work. I can add a test hook see them activate
when I use M-x narrow-to-region
and M-x widen
. However, when I use
other narrowing functions, like narrow-to-defun
, my hook functions
aren’t called.
Is there a narrowing primitive I missed? I check the source
code. Nope, these are lisp functions which ultimately call
narrow-to-region
. Is the advice not getting used when called
indirectly? I test that out.
(defun foo ()
(interactive)
(narrow-to-region 1 2))
This works fine. Hmmm, these other functions are byte-compiled, maybe
that’s the problem.
Bingo. The advice has stopped working. It has something to do with
byte-compilation.
Bytecode
Let’s take a look at the bytecode for foo
.
(symbol-function 'foo)
;; => #[nil "\300\301}\207" [1 2] 2 nil nil]
I don’t know too much about Emacs’ byte code, but here’s the gist of
it. A compiled function is a special type of vector (hence the #[]
form). This is a legal s-expression which you can use directly in
regular Elisp code just like it was a function. The only reason you’d
do so is for obfuscation, so it would look very suspicious.
The first element of this function vector is the parameter list —
empty in this case. The second is a string containing the actual
bytecodes. The rest holds the various constants from the function
body. This includes the symbols of other functions called by this
function. It’s important to note that narrow-to-region
does not
appear in this list!
Curious. Let’s take a closer look at the bytecode.
(coerce (aref (symbol-function 'foo2) 1) 'list)
;; => (192 193 125 135)
Looking at bytecomp.el
from the Emacs distribution I can see that
codes 192 and 193 are used for accessing constants. This pushes my
constants 1 and 2 onto a stack for use as function arguments. Next up
is 125, which corresponds to byte-narrow-to-region
. Gotcha!
It turns out narrow-to-region
is so special — probably because it’s
used very frequently — that it gets its own bytecode. The primitive
function call is being compiled away into a single instruction. This
means my advice will not be considered in byte-compiled code. Darnit.
The same is true for widen
(code 126).
Where to go now?
Since it’s not possible to hook or advise the buffer-narrowing
primitives, impatient-mode would need to hook some other event that
tends to happen at the same time. Perhaps any time a command is
executed in the current buffer it could check for changes to the
buffer restriction and, if so, update any attached web clients. I’ll
figure something out.