One more indentation hack
By Didier Verna on Wednesday, July 20 2011, 17:22 - Lisp - Permalink
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 (X)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 (X)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.PACKAGE.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 (X)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 package 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 (X)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 (X)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 package 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 in my package will look like this:
#i(econd cond) (defmacro econd #|...|#)
Pretty cool, eh?
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.package.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 (X)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 package-specific option to non-nil before loading the package (possibly setting them back to nil afterwards). They also need to trust that I'm not going to inject anything suspicious into their (X)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.
That's it. Happy indenting!
Comments
I was looking over the code in slime-cl-indent.el, and it looks to me as if we should be adding hash table values to common-lisp-system-indentation rather than adding values to the property 'common-lisp-indent-function on the symbol to be indented.
The former method, AFAICT, is intended to enable SLIME to distinguish between symbols with the same name that live in different packages. Setting the property values doesn't seem to be able to take CL package information into account, since emacs-lisp doesn't have a notion of package.
I have a number of confusions about this, because the code to allow CL to announce how it would like to be indented is all there, but it seems oddly structured. If CL is supposed to be specifying its own indentation, why doesn't it fill a hash table *on the CL side*, and have emacs-lisp consult this CL hash table, rather than having to breach the security protection against invoking emacs lisp from the CL side?
Many thanks for writing up some examples of this stuff: we could really use it!
Good point. I don't know for sure, but I suspect the reason is historical. slime-cl-indent is based on cl-indent which is an Emacs library with no relation to Slime, so the question of storing stuff on the CL side doesn't make any sense for it.
The other reason is that Emacs's indentation code (speaking for XEmacs here, but I suspect it is the same in GNU Emacs) is not very well abstracted. It is a mix of general functions and mode-specific stuff. I don't know for sure (I would need to check the code back) but it's possible that in order to make Emacs consult the CL side, you would need to modify code that applies to other modes than Lisp as well.