One More Indentation Hack

Here’s yet another indentation hack that I came up with recently.
All the work done by Nikodemus on the Slime indentation contrib is pretty cool, especially the notion of indentation style (though I wish the styles were Custom variables, but that is another story). I tend to use indentation styles for global, maybe collaborative preferences, but on several occasions however, I find that this approach has a couple of drawbacks.
- One of them is that the indentation information is far away from the corresponding symbol, in a separate file. If you change a function’s prototype for instance, you may also need to load the file(s) in which the corresponding style(s) is (are) defined and edit them.
- The other problem is that if you want to let other people edit your source code and honor your indentation style, you also need to provide them with the style definition, and they need to load it separately.
For those reasons, I tend to think that the indentation style approach is not very well suited for project-specific indentation settings. What I would like is to provide indentation information close to the function definition, and also to have that information automatically available when anyone loads the project into Slime. Here’s a way to do it.
The key to success here is the function swank:eval-in-emacs
which, as its
name suggests, sends some Emacs Lisp code to your Emacs session for
evaluation. This function effectively allows you to trigger some Emacs Lisp
computation from a Common Lisp file. Remember that indentation information is
stored in the common-lisp-indent-function
property of a symbol. The function
clindent
below does this:
(defun clindent (symbol indent)
"Set SYMBOL's indentation to INDENT in Emacs.
This function sets SYMBOL's `common-lisp-indent-function' property.
If INDENT is a symbol, use its indentation definition.
Otherwise, INDENT is considered as an indentation definition."
(when (and (member :swank *features*)
(let ((configuration
(find-symbol "MY.LIBRARY.CONFIGURATION" :cl-user)))
(when (and configuration (boundp configuration))
(getf (symbol-value configuration) :swank-eval-in-emacs))))
(funcall (intern "EVAL-IN-EMACS" :swank)
`(put ',symbol 'common-lisp-indent-function
,(if (symbolp indent)
`(get ',indent 'common-lisp-indent-function)
`',indent))
t)))
As explained in the docstring, this function will ask Emacs to put SYMBOL
’s
common-lisp-indent-function
property to a definition, either provided
directly, or retrieved from another symbol. For example, if your library
defines an econd
macro, you may want to call it like this:
(clindent 'econd 'cond)
This function ensures that Swank is actually available before using it (first
condition in the and
clause). I will explain the other weird bits later on.
The next question is when exactly do we want to call this function? The answer
is: pretty much on all occasions. Your code might be loaded from source and
interpreted, or it might be compiled. But then, it might be compiled within or
outside a Slime environment. In any case, you want your indentation
information to be sent to Emacs everytime it’s possible. So obviously, we’re
gonna wrap this function in an eval-when
form thanks to a macro. This is
also a good opportunity to save some quoting.
(defmacro defindent (symbol indent)
"Set SYMBOL's indentation to INDENT in Emacs.
SYMBOL and INDENT need not be quoted.
See `clindent' for more information."
`(eval-when (:compile-toplevel :execute :load-toplevel)
(clindent ',symbol ',indent)))
And now, right on top of your econd
definition, you can just say this:
(defindent econd cond)
Now here’s one final step. If your library uses its own readtable, it’s even
more convenient to define a reader-macro for indentation information. I choose
#i
:
(defun i-reader (stream subchar arg)
"Read an argument list for the DEFINDENT macro."
(declare (ignore subchar arg))
(cons 'defindent (read stream)))
(set-dispatch-macro-character #\# #\i #'i-reader *readtable*)
And now, the code will look like this:
#i(econd cond)
(defmacro econd #|...|#)
All right. We still have two weirdos to explain in the clindent
function.
First, you noticed that the function’s computation is conditionalized on the
existence of a cl-user::my.library.configuration
variable, which actually
stores a property list of various compiling or loading options for this
package. The option we’re interested in is :swank-eval-in-emacs
, which must
be set to non-nil. Here’s why. The execution of Emacs Lisp code from Swank is
(rightfully) considered as a security risk so it is disabled by default. If
you want to authorize that, you need to set the (Emacs) variable
slime-enable-evaluate-in-emacs
to t
. Otherwise, calling
swank:evaluate-in-emacs
is like calling 911. So we have a chicken-and-egg
problem here: if we want to avoid an error in clindent
, we would need to
check the value of this variable, but in order to do that, we would need to
evaluate something in Emacs… 🤨
The solution I choose is hence to disable the functionality by default, and
document the fact that if people want to use my indentation information, they
need to set both the Slime variable and my library-specific option to t
before loading the library (possibly setting them back to nil
afterwards).
They also need to trust that I’m not going to inject anything suspicious into
their Emacs session at the same time…
The last bit we need to explain is the final t
argument passed to
swank:eval-in-emacs
. The corresponding parameter is called nowait
in the
function’s prototype. It has something to do with asynchronous computation,
and in fact, I don’t really know what’s going on under the hood, but what I do
know is that if you set it to t
, Swank doesn’t care about the return value
of your form anymore, which is fine because we’re only doing a side effect. On
the other hand, if you omit that parameter, Swank will try to interpret the
return value in some way, and you will most probably get a serialization
error. Indeed, the return value is the indentation definition itself, so for
example, (&rest (&whole 2 &rest 1))
doesn’t make (Common Lisp) sense.