06 Apr 2015
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
.
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 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.
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 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!
02 Apr 2015
Intro
I just finished this feature today. Almost all Paredit functionality is available in terms of
lispy's own functions. The only functions that I didn't
implement were the ones that I didn't find any use for: paredit-backslash
(ugh, prompts),
paredit-comment-dwim
(covered by regular ;), paredit-forward-down
and
paredit-backward-down
(both just move out of special, and throw a lot).
Setting the keymap theme
So if you ever thought that lispy
could be a good idea, but the non-special key bindings are weird,
and you're too used to Paredit, you can now try this setting:
(lispy-set-key-theme '(special paredit c-digits))
Here's the setting that I'm using, since I love lispy
's default bindings:
(lispy-set-key-theme '(oleh special lispy c-digits))
The default setting actually is this one, lispy-mode-map-oleh
is an additional map for use with
my Xmodmap setup:
(lispy-set-key-theme '(special lispy c-digits))
Each item in the list passed to lispy-set-key-theme
designates a keymap to turn on.
So in a non-LISP buffer you could turn on just the special map:
(lispy-set-key-theme '(special))
This would mean that zero regular bindings get overridden, but when the point is before or after a
paren, you get the lispy
bindings. Actually, I've just enabled lispy-mode
for markdown-mode
,
and it's working great. Unfortunately, I haven't yet figured out how to make a minor mode's keymap
buffer-local. Maybe someone reading has an idea.
The screencast
I went through all of Paredit tests and made ERT tests out of them, to make sure that the features
are actually working. I've recorded a short (<3 minutes)
screencast of implementing one of the tests. Have a
look, maybe you'll something interesting in my setup. There are two short pauses in the video,
that's just me thinking how to write down a function. My ERT and Magit workflows are also shown.
Outro
The new key-theme feature is meant to make lispy
super-not-annoying. I think it's useful, since
using lispy
in Paredit mode was annoying for me, so the opposite has to be true for people that
are used to Paredit. So if you notice a bug, please report it, bug
reports are very important for my non-annoyance agenda.
Finally, if you're super-devoted to Paredit and don't want to drop it, now you can easily have both
lispy-mode
and paredit-mode
on. Just use this setting:
(lispy-set-key-theme '(special))
Or this one if you additionally want C-1 to show the inline doc, and C-2 to show
the inline args:
(lispy-set-key-theme '(special c-digits))
01 Apr 2015
Seriously, check the release notes if you
don't believe me.
Fixes
- Add
minibuffer-inactive-mode
to the lispy-elisp-modes
list. It means that you can eval there if you want.
- V (
lispy-visit
) should turn on projectile-global-mode
if it's not on.
- M (
lispy-multiline
) works better for Clojure: the regexes for vectors, maps and sets have been improved.
- C-k should not delete only the string when located at start of string.
- M will not turn vectors into lists any more.
- the backquote bug for i and M was fixed.
- you can flatten Elisp closures as well, at least the plain ones.
New Features
b calls lispy-back
The movement commands, such as:
- the arrows hjkl (
lispy-left
, lispy-down
etc.)
- f (
lispy-flow
)
- q (
lispy-ace-paren
)
- i (
lispy-tab
), only when called for an active region
will not store each movement in the point-and-mark history. You can press b to go back in
history. This is especially useful for h, l, and f, since they are
not trivially reversible.
b was previously bound to lispy-store-region-and-buffer
, so you could do Ediff with
b and B. Now it's bound to xB.
C-d (lispy-delete
) when positioned at the start of a comment, and with only whitespace
before the start of the line, will delete the whole comment.
If you want to un-comment, just use C-u ; from any point in the comment.
Added flatten operation for Clojure
xf (lispy-flatten
) now also works for Clojure, before it was only for Elisp.
Example 1 (flatten a macro):
|(->> [1 2 3 4 5]
(map sqr)
(filter odd?))
When you press xf you get this:
|(filter odd? (map sqr [1 2 3 4 5]))
Example 2 (flatten a standard function):
Start with:
After xf:
(let [f odd? coll [1 2 3 4 5]]
(lazy-seq (when-let [s (seq coll)]
(if (chunked-seq? s)
(let [c (chunk-first s)
size (int (count c))
b (chunk-buffer size)]
(dotimes [i size]
(chunk-append b (f (.nth c i))))
(chunk-cons (chunk b)
(map f (chunk-rest s))))
(cons (f (first s))
(map f (rest s)))))))
A bit of a gibberish, but at least we can confirm that map
is indeed lazy.
Example 3 (flatten your own function):
Example function:
This one requires the function to be properly loaded with C-c C-l (cider-load-file
),
otherwise Clojure will not know the location of the function.
Example statement:
After xf:
(+ |(let [x 10]
(* x x)) 20)
Added lax eval for Clojure
This is similar to the lax eval for Elisp. If you mark an expression with a region:
and press e, you will actually eval this:
(do (def asdf [1 2 3])
asdf)
You can do this for let bindings, it's super-useful for debugging. The rule is that if the first
element of the region is a symbol, and there's more stuff in the region besides the symbol, a lax
eval will be performed.
e will auto-start CIDER
If CIDER isn't live, e will start it and properly eval the current statement.
2F will search for variables first
Since Elisp is a LISP-2, there can be a function and a variable with the same name. F
(lispy-follow
) prefers functions, but now 2F will prefer variables.
Starting with:
|(filter odd? (map sqr [1 2 3 4 5]))
Pressing 2e gives:
(filter odd? (map sqr [1 2 3 4 5]))
;; =>
;; (1 9 25)
This works for all dialects, so you can also have:
(symbol-function 'exit-minibuffer)
;; =>
;; (closure (t)
;; nil "Terminate this minibuffer argument." (interactive)
;; (setq deactivate-mark nil)
;; (throw (quote exit)
;; nil))
or
*MODULES*
;; =>
;; ("SWANK-ARGLISTS" "SWANK-FANCY-INSPECTOR" "SWANK-FUZZY"
;; "SWANK-UTIL" "SWANK-PRESENTATIONS"
;; "SWANK-TRACE-DIALOG" "SB-CLTL2")
To do the last eval you need to be in special.
It means that you first have to mark the symbol *MODULES*
with a region.
A convenient function to mark the current symbol is M-m (lispy-mark-symbol
).
y (lispy-occur
) now has an ivy
back end
lispy-occur
launches an interactive search within the current top-level expression, usually a
defun. This is useful to see where a variable is used in a function, or to quickly navigate to a
statement.
You can customize lispy-occur-backend
to either ivy
(the default) or helm
(if you have it,
since it's no longer a dependency of lispy
).
Add ivy back end to lispy-completion-method
Now it's the default one for navigating to tags. You can select alternatively helm
or ido
if you
wish.
Remove the dependency on ace-jump-mode
Instead the dependency on ace-window
will be re-used. This allows for a lot of code
simplifications and better tests.
New custom variables:
lispy-avy-style-char
: choose where the overlay appears for Q (lispy-ace-char
)
lispy-avy-style-paren
: choose where the overlay appears for q (lispy-ace-paren
)
lispy-avy-style-symbol
: choose where the overlay appears for a (lispy-ace-symbol
)
and - (lispy-ace-subword
) and H (ace-symbol-replace
).
There's also lispy-avy-keys
, which is a
... z
by default.
Add lispy-compat
This is a list of compatibility features with other packages, such as edebug
and god-mode
.
They add overhead, so you might want to turn them off if you don't use the mentioned packages.
F works for Scheme
You can navigate to a symbol definition in Scheme with F. This feature was already
in place for Elisp, Clojure and CL.
Outro
Looks like a great batch of features, but I'm the most happy about the stupid backquote bug being
fixed.
Up next: pre-defined key binding themes, featuring:
- the beloved
default
.
- the old-school
paredit
.
- the special-only
minimal
.
- and maybe one more.
Feel free to chime in, if you've got a key setup that you think other people will like. Happy
hacking!
30 Mar 2015
This post might be interesting to people who use Travis CI to test their Elisp packages.
I've noticed a few days ago that my tests on Travis CI started failing randomly. Turns out that the
widely used ppa:cassou/emacs has become
deprecated. It still sort of works, but sometimes apt-get update
just times out, and I get a build
error.
Since it was the only one that I was using, I had to find another one. I went with emacs-snapshot
from ppa:ubuntu-elisp.
Here's my current .travis.yml:
language: emacs-lisp
env:
matrix:
- emacs=emacs-snapshot
before_install:
- sudo add-apt-repository -y ppa:ubuntu-elisp
- sudo apt-get update -qq
- sudo apt-get install -qq $emacs
- curl -fsSkL --max-time 10 --retry 10 --retry-delay 10 https://raw.github.com/cask/cask/master/go | python
script:
- make cask
- make test
I've adopted one more change in the above code: EMACS
is replaced by emacs
.
The reason for this is that both ansi-term
and compile
for legacy reasons set EMACS
to strange values,
like "24.4.91.2 (term:0.96)"
.
So if you have in your Makefile:
you're going to have a bad time. So I changed my Makefile to:
And now I can compile and test while inside Emacs with no issues. The only issue left is that I
don't yet know which PPA I should use for a stable Emacs. Maybe someone has a suggestion.
28 Mar 2015
Yesterday, I've added a new binding to swiper-map
: C-l will now call
swiper-recenter-top-bottom
.
The implementation is really easy, almost nothing to write home about:
(defun swiper-recenter-top-bottom (&optional arg)
"Call (`recenter-top-bottom' ARG) in `swiper--window'."
(interactive "P")
(with-selected-window swiper--window
(recenter-top-bottom arg)))
An interesting thing that I want to mention though is the customization of the default
recenter-top-bottom
behavior. This is the default one:
(setq recenter-positions '(middle top bottom))
And this is the logical one that I'm using:
(setq recenter-positions '(top middle bottom))
Try it out, and see if it makes sense to you. For me, when I've just jumped to a function
definition, which usually means that the point is on the first line of the function, the first
recenter position has to be top
, since that will maximize the amount of the function body that's
displayed on the screen.
Another use-case is when I'm reading an info or a web page. After a recenter to top, all that I have
read is scrolled out of view, and I can continue from the top.