(or emacs irrelevant)

Rule-based multi-line in lispy

Where 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.

lispy-test-eval-and-insert.png

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.

lispy-test-eval-and-replace.png

The sequence of actions in the test:

  • e calls lispy-eval to set foo to 42.
  • 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.

lispy-test-flatten.png

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-test-oneline.png

lispy-alt-multiline Demo 1

In the following image, I just press T once, starting from an unchanged buffer:

lispy-alt-multiline-1.gif

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 be equal to the read 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!