Skip to content

Latest commit

 

History

History
569 lines (435 loc) · 18.1 KB

README.adoc

File metadata and controls

569 lines (435 loc) · 18.1 KB

Editing Lisp and Scheme files in Neovim

Neoscmindent is a code indenter for Lisp and Scheme for the vi family of text editors.

This reference is written in a way that lets you start at the top and stop as soon as you have enough to get things working sufficiently well for you. Read the rest as and when you run into problems or gain interest.

1. Installation

If you’re using Neovim or Vim:

git clone https://github.com/ds26gte/neoscmindent
cd neoscmindent
./install.sh

That’s it.

Go to the section Alternative installation approaches if you think you need more control over the installation.

2. Indentation strategy

Lisp indentation has a tacit, widely accepted convention that is not lightly to be messed with. Users of the Emacs editor — often considered the gold standard for Lisp editing — will note that Neoscmindent provides a very similar style and customization. (Software teams using both editors will be able to collaborate on Lisp files without any indentation mismatch.)

Lisp code is essentially recursive lists of ultimately atoms. We call these code constituents forms. A form, if it is a non-empty list, has a head subform and zero or more argument subforms. Thus:

(head-subform arg-subform-1 arg-subform-2 ...)

Indenting Lisp code adds or removes spaces before each line so that the code has a quickly readable structure. Indenting does not change the content of a code line, and therefore, cannot add or remove or collapse lines.

Here are the default rules for how Neoscmindent goes about indenting Lisp code:

1: If the head subform is an atom and is followed by at least one other subform on its own line, then all subsequent lines in the form are indented to line up under the first argument subform. E.g.,

(some-function-1 arg1
                 arg2
                 ...)

2: If the head subform is an atom and is on a line by itself, then its subsequent lines are indented one column past the beginning of the head atom. E.g.,

(some-function-2
  arg1
  arg2
  ...)

3: If the head subform is a list, then its subsequent lines are indented to line up under the head subform. It does not matter whether there are argument subforms on the same line as the head form or not. E.g.,

((some-function-3 s-arg1 ...)
 arg1
 arg2
 ...)

4: If the head form is a literal (a non-symbolic atom, such as a number), then its subsequent lines are indented to line up directly under the head literal. It does not matter whether there are argument subforms on the same line as the head form or not. E.g.,

(1 2 3
 4 5 6)
'(alpha
  beta
  gamma)

(In the last example, the list is quoted, so its elements are considered literal, even though in general alpha would not be a literal.)

If this were all there is to it, it would make for rather boring indentation. So there is one exception thrown into the mix, for when the head subform is a symbol that we want to treat as a special keyword. A keyword is a symbol that has a Lisp Indent Number, or LIN, associated with it. (This is almost exactly analogous to Emacs’s lisp-indent-function, except I’m using numbers throughout.) The section on customization tells you how to set LINs. Let us call a keyword an N-keyword if it has a LIN of N.

5: If a form whose head is an N-keyword is split across lines, and if its i’th subform starts a line, then that subform’s indentation depends on the value of i relative to N.

5a: If i ≤ N, then the i’th subform is indented 3 columns past the beginning of the head keyword.

5b: If i > N, then the i’th subform is indented just one column past the beginning of the head keyword.

Examples:

(keyword-3 arg1
    arg2
    arg3
  arg4
  ...)
(keyword-3 arg1 arg2 arg3
  arg4
  ...)

3. Customization

You can use a customization file to modify Neoscmindent’s indenting. The name of the file is the first available of the following:

  • the value of the environment variable NVIM_LISPWORDS, if set

  • the value of the environment variable LISPWORDS, if set

  • the file ~/.lispwords.lua

If neither environment variable is set and ~/.lispwords.lua doesn’t exist, no customization is done. If either of the environment variables is set but the thus named file doesn’t exist, no customization is done.

The repo includes sample customization files, and the quick install takes care to set the environment variables (within the editor, not in your global shell environment). You can modify to taste.

(There is an intentional difference in the precedence of the environment variables depending on which Vim option is used to invoke Neoscmindent; we’ll get into that later, in the section on The option 'equalprg'.)

Here is an example customization file: It’s simply a Lua file that returns a Lua table associating keywords with their proposed LINs:

return {
  ['call-with-input-file'] = 1,
  ['case'] = 1,
  ['do'] = 2,
  ['do*'] = 2,
  ['fluid-let'] = 1,
  ['lambda'] = 1,
  ['let'] = 1,
  ['let*'] = 1,
  ['letrec'] = 1,
  ['let-values'] = 1,
  ['unless'] = 1,
  ['when'] = 1,
}

Neoscmindent also checks the option 'lispwords' (aka 'lw') for the LIN of a keyword that it can’t find in the customization file. Such keywords are assumed to have LIN 0.

If a keyword is specified in both the customization file and 'lispwords', the former takes precedence.

If a keyword is neither in the customization file nor in 'lispwords', but starts with def, its LIN is taken to be 0. (This is because Lispers tend to create ad hoc definer keywords, whether procedure or macro, whose names start with def, and they expect such keywords to not indent their subforms excessively, as rule 1 would require.)

All other keywords have LIN −1. These keywords follow the rules 1 and 2 above. You shouldn’t need to explicitly set a LIN of −1, unless the keyword is already in 'lispwords' (hence LIN 0), and you need to force it to behave like an ordinary symbol.

If you ever want a keyword to behave like a literal (rule 4), then set its LIN to −2.

3.1. A note on if

The keyword if is in 'lispwords', so by default it has LIN 0. if typically has 2 or 3 subforms. (In Common Lisp and some older Schemes it has 2 to 3; in modern Schemes, exactly 3; in Emacs Lisp, 2 to ∞.) Its first subform — the test subform — is almost always on the same line as the if. And since the LIN is 0, every subform under it is aligned 1 column to the right of the if, per rule 5b, like so:

(if test
  then
  else)

Some people like it. Many don’t: Here are three alternative LINs for if:

1: Set LIN to −1. Rule 1 holds:

(if test
    then
    else)

Since −1 is the default LIN for a keyword not in 'lispwords', you could either remove if from 'lispwords' (global or local to your filetype), or set its LIN explicitly to −1 in the customization file.

(Racket house style requires LIN −1, so if you’re OK with Racket, you can skip the rest of this section.)

2: Set LIN to 2. Then, per rule 5a and 5b:

(if test
    then
  else)

This has the advantage of distinguishing the then- and else- clauses.

3: Set LIN to 3. This indents both the then- and else-clause to be 3 columns to the right of if. It just so happens that if and its post-token space take up 3 columns, so you get the same result as LIN −1. Well, almost.

In the rare case you break the line before the then-clause, LIN −1 gives you, per rule 2:

(if
  test
  then
  else)

whereas, with LIN 3, rule 5a takes over:

(if
    test
    then
    else)

Which seems better? Another difference shows up if you have more than one else-clause (this is allowed in Emacs Lisp). With LIN −1, per rule 1:

(if test
    then
    else1
    else2
    ...)

With LIN 3, per rules 5a and 5b:

(if test
    then
    else1
  else2
  ...)

which seems objectively horrid. With LIN 2, also per rules 5a and 5b:

(if test
    then
  else1
  else2
  ...)

which seems better because it keeps the else-subforms together but distinct from the (single) then-form. In sum, go with LIN −1 if you want the then- and else-forms aligned; or with 2 if you want them distinguished.

4. Alternative installation approaches

While the quick-install in section Installation works for most people, if you already have an extensive Lisp editing setup, you may wish to incorporate the essentials of Neoscmindent in a more flexible way.

Let’s deconstruct the quick install: It puts the neoscmindent repo under a pack subdirectory somewhere in your 'runtimepath' (aka 'rtp') or 'packpath' (aka 'pp'). (See :help packages.)

An explicit install lets you pick the 'pack' subdirectory. Assuming ~/.config/nvim is in your 'runtimepath', a suitable 'pack' directory is ~/.config/nvim/pack.

Ensure a relevant subdirectory exists to receive neoscminent:

mkdir -p ~/.config/nvim/pack/3rdpartyplugins/start

Go there and clone this repo:

cd ~/.config/nvim/pack/3rdpartyplugins/start
git clone https://github.com/ds26gte/neoscmindent

(You don’t really need a plugin manager for this, but I expect that would work too, not that I’ve tried.)

If you don’t want to deal with packages at all, you can individually copy just the three essential files from the repo into your Neovim config area. The three files are:

autoload/scmindent.vim
lua/scmindent.lua
after/indent/lisp.vim

Again, unless you’re doing something atypical, your 'runtimepath' includes the directory ~/.config/nvim. First, ensure that the appropriate target directories exist:

mkdir -p ~/.config/nvim/autoload
mkdir -p ~/.config/nvim/lua
mkdir -p ~/.config/nvim/after/indent

Then, after `cd`ing to the repo directory, copy the three files over:

cp -p autoload/scmindent.vim ~/.config/nvim/autoload
cp -p lua/scmindent.lua ~/.config/nvim/lua
cp -p after/indent/lisp.vim ~/.config/nvim/after/indent

4.1. after/indent/lisp.vim

The after/indent/lisp.vim adds to the default indent plugin for Scheme and Lisp files some canned stuff that will let Neoscmindent do its thing. You may already have such a file, or wish to roll your own. In that case, do not copy this file over, or if you installed the entire repo under a 'pack' directory, delete this file.

If you want to create or modify your own after/indent/lisp.vim, make sure it does the following:

1: For Neovim, unset the 'lisp' and 'equalprg' (aka 'ep') options, and set 'indentexpr' (aka 'inde') to the indenting function:

setl nolisp
setl equalprg=
setl indentexpr=scmindent#GetScmIndent(v:lnum)

2: For Vim, unset the 'lisp' option and set 'equalprg' to scmindent.lua as a filter:

setl nolisp
setl equalprg=scmindent.lua

If scmindent.lua is not in your PATH, use an explicit pathname, e.g.,

setl equalprg=~/.config/nvim/lua/scmindent.lua

If you’re wondering why you don’t need an after/indent/scheme.vim, this is because Vim’s indent/scheme.vim takes care to load any and all indent/lisp.vim files that are present. For other Lisp-like files with a different filetype, you would add these lines to their specific after/indent file.

You can avoid an after file by explicitly assigning these options via a filetype autocommand, either in your init.vim or in a regular plugin file in your plugin directory, e.g.,

autocmd filetype scheme,lisp
   \ setl nolisp ep= inde=scmindent#GetScmIndent(v:lnum)

or

autocmd filetype scheme,lisp
   \ setl nolisp ep=scmindent.lua

Again, add other filetypes to the command above as needed.

4.2. A tale of three options: 'lisp', 'equalprg', and 'indentexpr'

Because of vi’s tortuous history, there are now three competing options that control Lisp indentation: 'lisp' and '`equalprg'` are available in all members of the vi text-editor family, whereas 'indentexpr' is available only in Vim and Neovim.

There are two aspects to indentation:

  1. Auto-indentation, or automatically indenting code as you type it.

  2. Re-indentation, or using the = command (in normal mode) to re-indent a contiguous region of one or more lines, called a range in Vim parlance. You can also use == to indent just the current line.

The options 'lisp' and 'equalprg' are less featureful than 'indentexpr'. Since the options compete for precedence in byzantine ways, in our default setup for Neovim, we simply unset 'lisp' and 'equalprg'. This ensures that 'indentexpr' is solely responsible for both aspects of indentation, which is usually what we want.

Sometimes, though, it may make sense to choose 'equalprg' over 'indentexpr', or, in rare situations, to even set both.

Here’s how the precedence between the three options shakes out:

  • Autoindentation: lisp > indentexpr

  • Re-indentation: equalprg > lisp > indentexpr

4.2.1. The option 'lisp'

Typically, the options 'lisp' and 'showmatch' (aka 'sm') are set together. Assuming 'equalprg' and 'indentexpr' are unset, 'lisp' takes care of both auto- and re-indentation. Except in the improved vi clones Vim and Neovim, this approach fails in at least two respects:

  1. escaped parentheses and double-quotes are not treated correctly; and

  2. 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 to identify keywords, continues to fail in strange ways. Neovim inherits this legacy.

4.2.2. The option 'equalprg'

Fortunately, vi (including Vim and Neovim) lets you delegate the responsibility for re-indentation to an external filter program of your choosing. The option used is 'equalprg', so called because it determines the program used for the = command.

Indeed, you can use the lua/scmindent.lua file in this repo as one such filter, viz.,

setl equalprg=scmindent.lua

as described above. (This is a local set used by Vim/Neovim, and is either used in an indent file, or in a general plugin file, inside a filetype-specific autocommand. For vi’s other than Vim/Neovim, you would just use set.)

To use scmindent.lua as a filter, you must have Lua on your system. If you can’t install Lua, you can consult Neoscmindent’s parent software, https://github.com/ds26gte/scmindent, which provides a choice of 'equalprg' filters in various languages.

Setting 'equalprg' only affects re-indentation. If 'lisp' is set, it still governs autoindentation, which can be confusing as the two options in general yield different results, and we already know 'lisp'’s algorithm is faulty. So it’s best to unset it:

setl nolisp

While this works, the experience is clunky because lines aren’t autoindented — and if they are, presumably because of an 'autoindent' (aka 'ai') setting, the indenting is very un-Lisp-like. To get your code correctly indented, you have to constantly remember to re-indent non-automatically, by explicitly typing = or == in normal mode every so often. Still, if you are OK with this extra effort, it will DTRT. It is also the only way of using scmindent.lua if you’re not using Neovim.

Note that this repo’s scmindent.lua, when used as an 'equalprg' filter, can be customized in almost the same way as for indentexpr. The only difference is that the environment variable LISPWORDS takes precedence over NVIM_LISPWORDS. This is a convenience: Unlike 'indentexpr', the 'equalprg' filter, being a purely external program, cannot access the 'lispwords' local option of the file that it’s indenting. Having a different customization file helps in explicitly adding the 'lispwords'-related information that the 'indentexpr' function takes for granted. In general, the customization file used for 'equalprg' will be larger than the one for 'indentexpr', because the latter doesn’t need to mention any of the 'lispwords', unless it wants to give them a LIN ≠ 0.

4.2.3. The option 'indentexpr'

In contrast to 'equalprg', the approach using 'indentexpr' offers the least friction. It works as a filter and also automatically indents your code as you type it. To let 'indentexpr' do both these tasks, you must unset 'lisp' and 'equalprg', as we’ve already described.

While 'indentexpr' is the superior option, our setting for it only works in Neovim, as it relies on the native Lua of this text editor. It will not work in Vim. It won’t work in other vi’s either, because they don’t have the 'indentexpr' option.

The after/indent/lisp.vim included in this repo works for both Vim and Neovim. It sets 'equalprg' for Vim and 'indentexpr' for Neovim.

4.2.4. Can you use both 'equalprg' and 'indentexpr'?

If 'equalprg' is not set, the 'indentexpr' function takes care of both auto- and re-indentation. It does the latter by being repeatedly called behind the scenes for every line in the range chosen for =.

For large ranges (e.g., the entirety of a large file), re-indenting based exclusively on 'indentexpr' can become noticeably slow, so much so that using an external filter can become competitive. In such cases, it may be worth your while to set 'equalprg' to scmindent.lua, while still retaining 'indentexpr' for autoindentation.

(It is also possible to set 'equalprg' to some other filter, but that risks a mismatch between the results produced by autoindentation versus re-indentation.)

4.2.5. Unset 'lisp' always!

'lisp' overrides 'indentexpr' for both autoindentation and re-indentation (arguably a design bug in Vim!), so it’s never advisable to set 'lisp' if 'indentexpr' is set.

If 'equalprg' is set, then 'lisp' only overrides it for autoindentation, but this is not terribly useful since the manual indentation by 'equalprg' will have to be used to correct 'lisp'’s faulty autoindentation anyway.

In essence, 'lisp' doesn’t play nice with either 'equalprg' or 'indentexpr', and when either or both of these are set, it’s best to simply unset 'lisp'.

There is one non-indentation benefit conferred by 'lisp', and that is that it allows keywords to contain -, the hyphen character (aka dash, minus). This is mildly useful, given Lisp identifiers can and often do have hyphens, but setting the 'iskeyword' (aka 'isk') option is a much better way to get this done.