Rule-based multi-line in lispy
06 Apr 2015Where do one-line expressions come from?
When programming LISP, especially with lispy, it's easy to
generate random one-line expressions. This is, of course, because the results of read
or eval
don't contain any whitespace information: all original newlines are lost.
Just to review the multitude of ways to insert generated code into a buffer using lispy
I'll list
the shortcuts and the test-based explanations (at around 2000 lines of tests and 54% test coverage,
lispy
is pretty well tested).
eval-and-insert
E calls lispy-eval-and-insert
.
The image above is generated using the interactive test visualizer lispy-view-test
, bound to
xv. If you want to explore how a certain command is intended to behave, just find the
corresponding test (with the same name as the command) and call xv.
eval-and-replace
xr calls lispy-eval-and-replace
. This function evaluates the current expression and
replaces it with the result.
The sequence of actions in the test:
- e calls
lispy-eval
to setfoo
to42
. - j calls
lispy-down
to move to the next sexp. - xr calls
lispy-eval-and-replace
.
Ideally, there should have been "xr"
instead of (lispy-eval-and-replace)
in the test, but
there's a small wrinkle in the lispy-with
macro that needs to be fixed before that can happen.
flatten
xf calls lispy-flatten
. This function expands in-place the current function or macro
call.
In this test, the misleadingly named function square
is evaluated and flattened, to see if the
&optional
and &rest
argument passing rules indeed work.
The flatten operation works really well for Elisp and quite well for Clojure. The CL implementation
would need to heavily rely on SLIME features (currently absent), since the CL spec doesn't define an
equivalent of Elisp's symbol-function
. The same applies to Scheme, I guess.
oneline
O calls lispy-oneline
. It's not eval-based, it just deletes the newlines.
If there are any comments present, they are pushed out.
lispy-alt-multiline
Demo 1
In the following image, I just press T once, starting from an unchanged buffer:
lispy-alt-multiline
Demo 2
Flatten push
Start from this code (the cursor is in the CSS, if you don't see it):
(let (res)
(dotimes (i 10)
( push i res))
(nreverse res))
After xf it becomes:
(let (res)
(dotimes (i 10)
( setq res
(cons i res)))
(nreverse res))
Since push
is a macro, macroexpand
is used. And since macroexpand
doesn't give newline
information, pp-to-string
is used, and it gives a reasonable result.
Flatten dotimes
Start with the same code, but with cursor on dotimes
this time:
(let (res)
( dotimes (i 10)
(push i res))
(nreverse res))
After xf it becomes:
(let (res)
( cl--block-wrapper
(catch '--cl-block-nil--
(let
((--dotimes-limit-- 10)
(i 0))
(while
(< i --dotimes-limit--)
(setq res
(cons i res))
(setq i
(1+ i))))))
(nreverse res))
This time pp-to-string
isn't as good: let
and while
statements are messed up.
Follow this up with T which calls lispy-alt-multiline
:
(let (res)
( cl--block-wrapper
(catch '--cl-block-nil--
(let ((--dotimes-limit-- 10)
(i 0))
(while (< i
--dotimes-limit--)
(setq res
(cons i
res))
(setq i
(1+
i))))))
(nreverse res))
Well, at least some parts look better. It could be make perfect by adding a sort of threshold when
printing each sub-expression. It's less than, say 15
chars, which (setq i (1+ i))
is, no
newlines should be added. I'll add this a bit later.
More on lispy-alt-multiline
lispy-alt-multiline
can be used on a LISP expression to re-format it across multiple lines. It
doesn't matter in which shape the expression currently is, since all current newlines will be removed
before the algorithm starts.
This has to be done with some rules, since a one-line expression can transform to multiple viable multi-line forms. So far, these rules are implemented by customizing these variables:
(defvar lispy--multiline-take-3
'(defvar defun defmacro defcustom defgroup)
"List of constructs for which the first 3 elements are on the first line.")
(defvar lispy--multiline-take-2 '(defface define-minor-mode
condition-case while incf car cdr > >= < <= eq equal incf decf
cl-incf cl-decf catch require provide setq cons when if unless interactive)
"List of constructs for which the first 2 elements are on the first line.")
The name suggests that there should be lispy-multiline
, and there is, bound to M. The
difference between M and T is that M is older and ad-hoc, while
T is newer and rule-based. This means that the latter can misbehave, since it's not yet
fully tested. However, it has the following built-in check to make sure that it doesn't mess up your
code:
The
read
of the expression before transformation should beequal
to theread
of the transformed expression.
If the above check fails, no change will be performed on the source code. So your code should be
pretty safe. One more cool thing that I want to add to other operations is that it checks if the
buffer will be changed after the transformation. If there will be no change, it will just issue a
"No change"
message, and no change will be performed. This is really cool if you obsess about the
buffer changed marker in the mode line like I do.
Outro
The functions bound to O, M, and T apply to the current expression.
To be really sure which one, turn on show-paren-mode
. You can also call these functions not from
special, although it's not very convenient. The typical strategy in that case would be to bind all
of them on a prefix map, e.g. C-c.
How is it different from special then?
Instead of typing [T you would type C-c T.
But the advantage of the special approach is that [ actually does something (moves point to the start of the current list), instead of just being a useless part of a key combination like C-c. And if you're in special already, there's no need for [.
I hope that you enjoy the new update. If it's needed, variables like lispy--multiline-take-3
can
be made buffer-local so that T works appropriately for Clojure, CL and Scheme, instead of
just Elisp. If you'd like to add support for your favorite dialect in this way, I'd be happy to
explain some details if needed and to merge the PR. Happy hacking!