No description, website, or topics provided.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


Editing Lisp and Scheme files in Neovim

Neoscmindent is a Neovim native-Lua implementation of an indenting function for Scheme, Lisp, and similar languages.


This package contains the files


Copy them to the autoload, lua, and after/indent subdirectories respectively, somewhere in your Neovim 'runtimepath' (aka 'rtp'). Unless you’re doing something atypical, your 'rtp'/'pp' includes the directory ~/.config/nvim, so you could do:

# ensure that the relevant subdirectories in your rtp exist
mkdir -p ~/.config/nvim/autoload
mkdir -p ~/.config/nvim/lua
mkdir -p ~/.config/nvim/after/indent
# go to the directory created by this repo
cd <this-repo-directory>
# copy the files from the repo to the appropriate places in rtp
cp -p autoload/neoscmindent.vim ~/.config/nvim/autoload
cp -p lua/neoscmindent.lua ~/.config/nvim/lua
cp -p after/indent/lisp.vim ~/.config/nvim/after/indent

Alternatively, clone this repo inside the pack subdirectory somewhere in your 'rtp' or 'packpath' (aka 'pp'; see :help packages). E.g., again assuming ~/.config/nvim is in your 'rtp', you could do

# ensure a relevant subdirectory exists to receive neoscminent
mkdir -p ~/.config/nvim/pack/3rdpartyplugins/start
# go there
cd ~/.config/nvim/pack/3rdpartyplugins/start
# clone this repo there
git clone

Either approoach is easy enough, you don’t really need a 3rd-party plugin manager, but I expect that would work too, not that I’ve tried.

The after/indent/lisp.vim is just for convenience. You may want to trigger the indenter in your own way, especially if you already have a familiar setup for your Scheme and Lisp (or other Lisp-y) files. Whatever you do, you have to make sure that the local option 'indentexpr' (aka 'inde') is set for the files that you want indented by Neoscmindent, i.e.,

setl inde=neoscmindent#GetScmIndent(v:lnum)

(If you’re wondering why the repo doesn’t provide 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.)

You also need to ensure that the options 'lisp' and 'equalprg' (aka 'ep') are not set, so that 'inde' has a chance to do its thing.

A note about 'lisp' and 'equalprg'

'lisp' is Vim’s own built-in attempt at indenting Lisp. It’s less featureful than Neoscmindent, and also occasionally buggy. Try correcting the indent on the following with 'lisp' on, after unsetting 'inde' and 'equalprg':

  (display "alpha
             bravo should stay put
               charlie should stay put")
       "should line up under lparen before display")

Also 'lisp' has no concept of indenting different subforms of the same form differently.

'equalprg' is used to call an external-program indenter, e.g., the kinds provided in Neoscmindent’s parent software,

If you’re using Vim — i.e., not Neovim — then you can still use Neoscmindent, but not as your 'indentexpr'. Instead set 'equalprg' as follows:

setl equalprg=neoscmindent.lua

after ensuring neoscmindent.lua is somewhere in your $PATH. This requires that you have Lua on your system. The experience will be clunkier than using 'indentexpr', because the autoindentation won’t be Lispy: you will have to call = explicitly every so often in order to correct the indentation. But it will DTRT.

Indentation strategy

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

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


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

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)

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


(keyword-3 arg1
(keyword-3 arg1 arg2 arg3


Neoscmindent uses keyword info from ~/.lispwords.lua. Here is an example .lispwords.lua: 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 for option 'lispwords' (aka 'lw') for the LIN of a keyword that it can’t find in .lispwords.lua. Such keywords are assumed to have LIN 0.

If a keyword is neither in '.lispwords' 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 which they expect 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.

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.) 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, like so:

(if test

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

1: Set LIN to −1.

(if test

Since −1 is the default LIN, it might appear all you need to do is to remove if from 'lispwords'. This would work, but you’d have to remove it from the global 'lispwords': Neoscmindent can’t read local modifications to 'lispwords'. If this is too much hassle, just set the LIN in ~/.lispwords.lua.

2: Set LIN to 2.

(if test

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 so happens 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


whereas, with LIN 3:


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

(if test

With LIN 3:

(if test

which seems objectively bad. LIN 2 would have:

(if test

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.