But then, when was the last time you heard of a lisp programmer that used vi? (Paul Fox, vile developer)
The ability to automatically indent code is indispensable when editing files containing s-expression-based code such as Racket, Common Lisp, Scheme, LFE, ART*Enterprise, and other Lisps.
The text editor family vi provides the option lisp
, which in
conjunction with the options autoindent
and showmatch
provides a form of Lisp indenting, but except in the improved vi
clones Vim and Neovim, this support is poor in at least two
respects:
-
escaped parentheses and double-quotes are not treated correctly; and
-
all head-words are treated identically.
Even the redoubtable Vim, which has improved its Lisp editing
support over the years, and provides the lispwords
option,
continues to fail in
strange
ways.
Fortunately, both vi and Vim let you delegate the responsibility
for indenting such code to an external filter program of your
choosing. I provide here four such filtering scripts:
scmindent.rkt
written in Racket, lispindent.lisp
in Common
Lisp, scmindent.lua
in Lua, and scmindent.js
in JavaScript.
The Racket
and CL scripts are
operationally identical and use the same type of customization
via the file ~/.lispwords
.
The Lua and JavaScript versions differ from the above in having their own format for the customization file. The Lua file is named .lispwords.lua and uses a Lua table. The JavaScript file is named lispwords.json and uses JSON.
To use either of scmindent.rkt
, lispindent.lisp
, scmindent.lua
, or
scmindent.js
, place them in your PATH
. Alternatively,
the JavaScript version is also available as a Node
package:
sudo npm install -g scmindent
This directly installs the executable scmindent
in your PATH
.
Henceforth, I will refer to just scmindent.rkt
with the understanding that
everything mentioned applies equally to lispindent.lisp
,
scmindent.lua
, scmindent.js
, and the NPM version scmindent
.
scmindent.rkt
takes
Lisp text from its standard input and produces an indented version
thereof on its standard output. (Thus, it is possible to use
scmindent.rkt
as a command-line filter to "beautify" Lisp code, even if
you don’t use vi.)
In Vim, set the equalprg
option to the filter name, which causes the
indenting command =
to invoke the filter rather than the built-in
indenter.
You might want to make the equalprg
setting local to a file
based on its extension:
autocmd bufread,bufnewfile *.lisp,*.scm setlocal equalprg=scmindent.rkt
or its filetype:
autocmd filetype lisp,scheme setlocal equalprg=scmindent.rkt
In vi’s other than Vim, use the !
command to invoke the filter on part or all of
your buffer: Type !
to declare you’ll be filtering; a movement command
to scoop up the lines you’ll be filtering; then the filter name
(scmindent.rkt
) followed by Return
.
Lisp indentation has a tacit, widely accepted convention that is not
lightly to be messed with, so scmindent.rkt
strives to provide the same
style as Emacs, with the same type of customization.
By default, the indentation procedure treats a form split over two or more lines as follows. (A form, if it is a list, is considered to have a head subform and zero or more argument subforms.)
1: If the head subform is followed by at least one other subform on the same line, then subsequent lines in the form are indented to line up directly under the first argument subform.
(some-user-function-1 arg1 arg2 ...)
2: If the head subform is a list and is on a line by itself, then subsequent lines in the form are indented to line up directly under the head subform.
((some-user-function-2) arg1 arg2 ...)
3: If the head subform is a symbol and is on a line by itself, then subsequent lines in the form are indented one column past the beginning of the head symbol.
(some-user-function-3 arg1 arg2 ...)
4: If the head form can be deduced to be a literal, then subforms on subsequent lines line up directly under it, e.g.,
(1 2 3 4 5 6)
'(alpha beta gamma)
However, some keyword symbols are treated differently. Each such
keyword has a number N associated with it called its Lisp Indent
Number, or LIN,
which influences how its subforms are indented. This is almost exactly
analogous to Emacs’s lisp-indent-function
, except I’m using numbers
throughout.
If the i’th argument subform starts on a subsequent line, and i ≤ N, then it is indented 3 columns past the keyword. All subsequent subforms are indented simply one column past the keyword.
(defun some-user-function-4 (x y) ;defun is a 2-keyword body ...)
(defun some-user-function-5 (x y) body ...)
(if test ;if is also a 2-keyword then-branch else-branch)
scmindent.rkt
pre-sets the LINs of many well-known
Lisp keywords. In addition, any symbol that starts with def
and whose
LIN has not
been explicitly set is assumed to
have an LIN of 0.
You can specify your own LINs for keywords via a customization file. The name of the file is the first available of the following:
-
the value of the environment variable
LISPWORDS
, if set -
the file
.lispwords
in your home directory
If LISPWORDS
isn’t set and ~/.lispwords
doesn’t exist, no
customization is done. If LISPWORDS
is set but the thus named
file doesn’t exist, no customization is done.
~/.lispwords
can contain any number of
2-element lists: The first element of each list is a Lisp symbol
and the second element is the LIN associated with
it. E.g.,
(begin0 0) (when 1) (unless 1) (do 2) (define 2)
This assigns a LIN of 0 to begin0
; 1 to
when
and unless
; and 2 to do
and defun
.
If the LIN is 0, it may be omitted and the enclosing parentheses
dispensed with. Thus, begin0
is short for (begin0 0)
.
As a convenience, you can bunch symbols with the same LIN together in one of two ways, e.g.,
(begin0 0) ((when unless) 1) ((do define) 2)
or
(0 begin0) (1 when unless) (2 do defun)
If using the JavaScript scmindent
, see below for the
corresponding lispwords.json
format.
(Note that in contrast
to Vim’s flat list of lispwords
, ~/.lispwords
allows for different categories of lispwords. Vim’s lispwords
are
all of LIN 0.)
For example, a lot of users prefer the keyword if
to have its then-
and else-clauses indented the same amount of 3 columns. I.e.,
they want it to be a 3-keyword. A .lispwords
entry that would
secure this is:
(if 3)
To remove the keywordness of a symbol, you can assign it a LIN < 0. E.g.
(if -1)
would also cause all of if
's subforms to be aligned. (This is because
−1 causes subforms on subsequent lines to line up against the first
argument subform on the first line, and that happens to be 3 columns
past the beginning of a 2-column keyword like `if
. The only difference
between −1 and 3 here is what happens when the if
is on a line by
itself, with the test on the line following. −1 indents subsequent
lines one column past the beginning of the if
, whereas 3 continues to
indent them three columns past the beginning of the if
. Further
differences emerge between 3 and −1 when the if
has more than three
argument subforms, as allowed by Emacs Lisp, where 2 and −1 immediately
prove to be better choices than 3. The author has made 2 the default
because it is the only option that has the merit of indenting the then-
and else-subforms by differing amounts.)
~/.lispwords.lua
, used by the Lua version, employs a different
format than ~/.lispwords
. It return
s a
Lua table, whose keys are strings corresponding to Lisp keywords,
and whose values are their corresponding LINs.
Keywords sharing the same LIN cannot be bunched.
E.g., the example .lispwords
above will be specified as follows
in .lispwords.lua
:
return { ['begin0'] = 0, ['when'] = 1, ['unless'] = 1, ['do'] = 2, ['defun'] = 2, }
lispwords.json
, used by the JavaScript version, employs a different format
than .lispwords
in order to accommodate JSON. Keywords are
specified as keys, the LINs as values, and
keywords sharing the same LIN cannot be bunched.
E.g., the example .lispwords
of the previous section will
be specified as follows in lispwords.json
:
{ "begin0": 0, "when": 1, "unless": 1, "do": 2, "defun": 2 }