lispy 0.21.0 is out
19 Jan 2015The last release was more than a month ago, and there have been more than 130 commits to master since then. Somehow, I've been dragging my feet with this release: the (3 pages of) release notes were in the draft stage for 10 days now, while I kept committing on top of them.
Introduction
This project is my vision of efficient LISP editing. According to Github, I started it more than a year ago, although the initial commit already contained around 1000 lines of code. After a year, it's more than 100 interactive commands in 5000 lines of code and 1000 lines of tests.
Initially, I started the project because, while I wanted to learn
Paredit to get more efficient, I did not want to learn Paredit's
cumbersome bindings. Having established a skeleton that allows to call
Paredit-like commands with plain letters, over time, I've tacked on
anything LISP-related here. In this way, it's very similar to
org-mode
, which starts with an outline and TODO skeleton, and then
adds everything else in the world on top of that. Heck, I actually
tacked on org-mode
's outline features on top of lispy
: when at an
outline, i is equivalent to org-mode
's TAB, and
I is equivalent to S-tab.
Among the other packages with which lispy
integrates/cooperates/coexists are: edebug
, ediff
, eldoc
, ert
,
outline
, semantic
, semantic/db
, ace-jump-mode
, iedit
,
delsel
, helm
, multiple-cursors
, fancy-narrow
, projectile
,
god-mode
, auto-complete
, company
.
Main idea behind lispy
The idea is to have plain letters, like h or r, call commands instead of self-inserting, but only if the point position is such that you wouldn't want to self-insert anyway. When this situation occurs, I like to say "the point is special"; this shortcut is all over the code and the docs.
This is a bit similar to vi
's normal/insert states, but instead of
"holding" the current state in your head, it's visible through just
the point position. And instead of having just the
Esc/i combination to toggle normal/insert state,
you can do it with any command that moves point,
e.g. C-f, or C-a, or even any custom command
that you write. lispy
does provide [, ], and
C-3 key bindings for getting into special, but you are free
to use any other binding or command that you want.
For instance, starting from this code and point position (which is already special):
(when (= arg 0)
( setq arg 2000))
you can move to the when
statement with h (lispy-right
):
( when (= arg 0)
(setq arg 2000))
or you can remove the when
statement altogether with r (lispy-raise
):
( setq arg 2000)
or you can:
- evaluate
(setq arg 2000)
statement with e (lispy-eval
); the result will be displayed in the echo area; works for multiple LISP dialects - evaluate and insert with E (
lispy-eval-and-insert
); the actual value2000
will be inserted below the expression - evaluate and replace with xr (
lispy-eval-and-replace
); the expression will be replaced with2000
- copy the statement to the kill ring with n (
lispy-new-copy
) - delete the statement with C-d (
lispy-delete
) - insert a copy of the statement below with c (
lispy-clone
) - insert 3 copies of the statement below with 3c (
digit-argument
,lispy-clone
) - move the statement on the previous line with DEL (
lispy-delete-backward
) - mark the statement with m (
lispy-mark-list
) - mark only
arg
with 2m (digit-argument
,lispy-mark-list
) - get help for
setq
with xh (lispy-describe
) - get help for
arg
with 2mxh (digit-argument
,lispy-mark-list
,lispy-describe
) - copy
2000
to kill ring with 3mn (digit-argument
,lispy-mark-list
,lispy-new-copy
) - move the statement outside of
when
with oh (lispy-other
,lispy-left
) - swap
(= arg 0)
with(setq arg 2000)
with w (lispy-move-up
) - move it back with s (
lispy-move-down
) - put the whole
when
expression on one line with hO (lispy-left
,lispy-oneline
) - narrow the buffer to current sexp with N (
lispy-narrow
) - widen the buffer with W (
lispy-widen
) - a lot of other things, there are 52 plain letters, after all
Note that in the following code, the point is not special:
(whe n (= arg 0)
(setq arg 2000))
so if you would type h, it would not call lispy-left
, but would insert "h" instead, yielding whhen
.
- to get the point into special before
(when
, you can use either C-a or [ or C-M-a - to get the point into special after
arg 0)
, you can use either C-e or ] - to get the point into special after
2000))
, you can use C-3
Evolution of special
Initially, the special state was only for the point before an open paren or after a close paren, since you almost never-ever want to insert characters at those positions. Over time, other point states were added to special:
- region is active (supersedes expand-region for LISP dialects)
- the point is at the start of a comment
- the point is at the start of an outline
Region selection is especially important, since it is super-useful for manipulating (move, copy, eval, get-help, goto-definition) symbols inside lists. Since these symbols aren't delimited with parens, the only way to get to them with special is though region selection.
Happily, region selection will fix the largest source of Paredit
unbalanced paren errors: while using lispy
region-manipulating
commands, you can't copy an unbalanced expression, and thus you can't
yank an unbalanced expression. You just have to use m and
M-m instead of C-SPC; and h,
j, k, l, i,
>, and < instead of e.g. C-f and
M-f.
Since there are only 26 lower-case letters and 26 upper-case letters,
the state of the command bindings in lispy
quickly turned into
survival of the fittest:
- the commands that were used the most got the priority bindings of lower case letters on the home row
- the second tier got the other lower-case bindings
- the third tier of commands were put on upper-case letters and on x + lower-case letters
- the fourth tier of commands are not bound at all, and I'm considering to obsolete some of them, just to keep things simpler.
ADD: the Annoyance-Driven Development
The other part of evolving and refining the commands, consisted of noticing small annoyances for when some generic command wasn't working as intended, or it was working in a sub-optimal way in a certain situation. After this, I would fix the command and put a test on it, to make sure that the annoyance does not surface in the future.
Notes on LISP dialects
My priority is Elisp, since that's what I'm using to implement
lispy
, but the following dialects are also supported:
To be supported, all a LISP dialect needs is just to use (
or {
or
[
as the opening delimiter; and )
or }
or ]
as the closing
delimiter, no actual adaptations in the lispy
code are necessary.
The only thing that needs to be implemented on a per-dialect basis is the language-specific eval:
- e (
lispy-eval
) - E (
lispy-eval-and-insert
) - xr (
lispy-eval-and-replace
) - xj (
lispy-debug-step-in
)
Also, though the jump-to-definition functionality could be implemented
via CEDET, environments like SLIME can do it much better, so
F (lispy-follow
) and M-. (lispy-goto-symbol
) use the
appropriate environment's facilities. Somehow, I still haven't managed
to implement this for Geiser.
Drinking from the lispy
fire hose
I'll get you started with the most basic and composable commands
below. You can find the rest in the
function reference or by just calling
xv (lispy-view-test
) on should
statements of
lispy-test.el:
In the screenshot above, I start with the code and point position on
the top. Then, after typing miji, I should end up with the
state below: the point and mark position have moved. In this
particular situation, I could follow-up with e to see the
value of auto-mode-alist
. Here's how to decipher miji:
- m -
lispy-mark-list
: marks current expression - i -
lispy-tab
(mnemonic for indent or inner): marks thecar
of current expression - j -
lispy-down
(vi shortcut to move down): moves the point and mark down by one sexp, selecting the quoted expression - i -
lispy-tab
: selects thecar
of the quoted expression, i.e.auto-mode-alist
As you see from the screenshot, I have show-paren-mode
always
on. It's even on in the tests visualization!
The most basic lispy
commands: the arrows
- h is left
- j is down
- k is up
- l is right
The directions are literal only if you have your code properly indented, with newlines after each sexp. Otherwise, it may be the case that j moves literally right, instead of down; still, it's down figuratively.
arrows like digits
All of them take digit arguments, so that e.g. 5j is equivalent to jjjjj.
h and j maintain the guarantee of not exiting the current list, so you can use e.g. 99j to move to the last element, if your list length is smaller than 99.
arrows like regions
When the region is active, the arrows move the mark appropriately with the point. You can activate and deactivate the region by repeatedly pressing m.
You can also mark a symbol with M-m
(lispy-mark-symbol
). There's no need to be in special for this
command to work. I call these type of bindings global, while the
bindings that only work in special I call local.
arrows like outlines
When located at the outline, j will call
outline-next-visible-heading
, and k will call
outline-previous-visible-heading
. l will move to the
first list of the outline, while h will jump between the
top-level sexp and the containing outline.
switching to a different side of the expression
Arrows can't do this easily; this can instead be done with
d (lispy-different
). Works for lists and regions.
Moving the code instead of moving around the code
The most basic commands are:
- w is
lispy-move-up
- s is
lispy-move-down
These will "hold on" to the current expression while moving it in the appropriate direction. There's no need to worry to mess up with them, since they cancel each other out perfectly.
Note that, just like with the arrows, if you don't have an opening or closing delimiter to "grab", you can mark a symbol M-m to be in special and use w / s.
Modified arrows can move too
o will modify the arrow keys temporarily, just for one command, with a minor mode. You can think of it as making the arrows move the point and the sexp in the usual direction, instead of moving just the point.
- ol: move current sexp outside of the parent list, forwards
- oh: move current sexp outside of the parent list, backwards
- oj: move current sexp inside the next list, making it the first element
- ok: move current sexp inside the preceding list, making it the last element
Extending or shrinking the current list or region
- > (
lispy-slurp
) grows the current list or region in the current direction by one sexp - < (
lispy-barf
) shrinks the current list or region in the current direction by one sexp
Similarly to j and k, these commands maintain the guarantee of not exiting the parent list, so you can slurp until the end of the list with e.g. 99>.
Outro
This project is very far from being final, I'm expecting to reach
0.99.0
before getting to 1.0.0
. The reason is that the package
aims to build intuition to the point of automation. For each small
step in that direction, every small bug is two steps back, since it
breaks the process of building intuition. So every possible situation
needs to be tested and bugs fixed until the package is finally ironed
out.
While I do appreciate the
stars, actually trying
to do things with lispy
and raising
issues would help me a
great deal more. For instance, if you raise an issue like
How can I generate a function call right after the function definition?
I would say to just use 2mcol(:
- mark the function name with 2m
- clone region with c
- move region outside the function body with ol
- wrap the region with parens while deactivating it with (
And this would be a sort of FAQ question / recipe already done there.
Or you could raise an issue like:
Why doesn't F work for Racket?
I would say it's because I haven't implemented it yet, since it was
tricky. But I'll get to it, now that I see that there's some interest
in lispy
from Racket users.