Open-source project
guns/vim-sexp avatar
guns/vim-sexp

vim-sexp: precision editing for S-expressions in Vim

Precision Editing for S-expressions

648 stars38 forksVim ScriptMIT

At a glance

What is it?
vim-sexp adds Lisp-style text objects, motions and indenting to Vim so that parentheses-heavy source can be edited by structure rather than by character counting. It targets Vim 8.0+ users who write Scheme, Common Lisp, Emacs Lisp or Clojure by hand.
Who is it for?
vim-sexp earns its place for people who spend whole days inside parentheses: the text objects, the element model and the structure-aware indenting are the whole point of the plugin. It is a poor fit for editors that are not Vim 8.0 or later, for anyone unwilling to learn a second motion vocabulary, or for Lisp dialects whose reader syntax differs sharply from Scheme's, since macro characters default to Scheme's set in an unknown FileType.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 22 days ago.
What is it written in?
Mainly Vim Script, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem vim-sexp targets: editing by shape, not by character

Vim is fast at moving through words and lines. It is far slower inside a Lisp file, where the interesting boundaries are the parentheses and the atoms they contain. Deleting the third argument of a nested call means counting opening brackets by eye, and the count changes as soon as the surrounding form is edited. vim-sexp addresses this directly: it gives Vim a notion of a compound form, of an element inside one, and of a child of a form, so that selection and movement happen along the structure of the expression rather than along the text.

The scope is narrow and deliberate. The plugin is written in Vim Script, targets Vim 8.0 and later, and describes itself as bringing the Vim philosophy of precision editing to S-expressions. There is no new language, no LSP, no parser of its own in the default configuration, and no project files to configure. Everything is expressed through text objects, motions and a handful of indent commands.

Who it is for is fairly easy to state. It suits people who hand-write Lisp-family code, or who edit configuration expressed in S-expression form, such as Guile, Emacs Lisp configuration, Racket, Scheme, or the subset of Clojure that sits inside Emacs. It also suits people who read a lot of Lisp but do not want to move to a dedicated Lisp editor, because vim-sexp keeps the modal editing model intact and adds only the missing structural vocabulary.

Installing the plugin in Vim and in Neovim

The README points at the plugin's own help file for the traditional installation path. Download the plugin into your runtimepath, or use a plugin manager that handles the placement for you. The help topic is where the detailed steps live:

code
:help sexp-installation

That single command is the whole of the documented traditional procedure. There is no build step, no compilation, and no runtime dependency to install, which is the main reason the plugin is easy to try.

Neovim works too, but with a caveat the README states directly: the traditional plugin load approach can work, yet some Neovim-only plugin managers may require a different approach. The project ships an example configuration for Lazy.nvim, and suggests it as a useful starting point even for people on a different manager:

code
:help sexp-lazy-install

The optional dependency is vim-repeat. Installing it is not required for the plugin to function, but it enables use of the dot command for repeating change operations performed through vim-sexp, and also for repeating built-in operations that use vim-sexp's text objects. That distinction matters in practice: without vim-repeat, a change made with a structural text object cannot be repeated with a single keystroke, which is a large part of the value of the plugin in normal editing sessions.

So the realistic installation order is: place the plugin, optionally add vim-repeat, and check that your plugin manager actually loads the files. If something fails to load under Neovim, the README invites an issue report.

Text objects and the element model behind them

The design is documented as a set of definitions, and those definitions do more work than any individual mapping. A compound form is a region of text delimited by a pair of parentheses, square brackets, or curly braces. A string is a contiguous region whose syntax name matches the Vim pattern for string, regex or pattern. A comment is a region whose syntax name matches the comment pattern, with the useful refinement that line comments which are indented and sit on successive lines count as a single comment. Macro characters are the leading characters a Lisp reader expands at read time, and these differ by language, defaulting to Scheme's set when the file type is unknown.

An element is then resolved by a fixed order of tests: the current string if the cursor is in a string, the current comment if the cursor is in a comment, the current compound form if the cursor sits on a paired structural bracket, the sequence of macro characters and the element that follows if the cursor is on a macro character, and otherwise the contiguous run of non-whitespace, non-bracket characters. An element always includes its leading macro characters, so a quoted or otherwise prefixed datum is edited as one unit rather than as loose characters.

Those definitions drive the text objects. The af and if objects select compound forms, aF and iF select top-level compound forms, as and is select strings, and ae and ie select elements. Two pairs address position inside a form rather than around the cursor: ac and ic select the Nth child element counting from the start of the current compound form, while aC and iC select the Nth child counting from the end. For the last case, the end is the one worth remembering, because the final argument of a call is usually the one you want to edit.

Motions: structural movement, element movement, and flow

Vim-sexp separates motions into three families, and the separation explains most of the plugin's behaviour.

Text object motions move the cursor in normal mode, extend selections in visual mode, and refer to the text defined by the movement of the cursor in operator-pending mode. The bracket motions move to the nearest paired structural bracket. The alt-b and alt-w motions move element-wise, ending on an element head, analogous to the built-in b and w. The alt-e and g-alt-e motions move element-wise ending on an element tail. The [[ and ]] motions move to an adjacent top-level element, and the [e and ]e mappings select an adjacent element.

Flow motions are looser by design. They move the cursor in normal mode and move rather than extend the selection in visual mode, and they are completely unconstrained by list structure, so the cursor can pass freely in and out of compound forms. There are list motions that land only on brackets, in both directions, and leaf motions that land only on non-list elements such as atoms, strings and comments. The README's own analogy is useful: square bracket commands tend to move down and into lists, curly braces up and out, and if you picture a top-level form as a tree, the square bracket pair performs forwards and backwards depth-first descents while the curly brace pair rewinds that descent.

One deliberate restriction stands out. Flow motions are not provided in operator-pending mode, because applying delete operators across list boundaries could destroy structural integrity. That is a trade-off the project makes openly: you cannot use a flow motion as the target of a delete or change, only as a way to position the cursor.

Indenting and end-of-line comment alignment

The indent commands cover the same ground as the motions. The == command indents the current compound form or visual selection without moving the cursor, and =- does the same for the top-level compound form in normal mode only. The alt-= command additionally removes extra whitespace, and alt-- applies that to a top-level compound form. So == and =- keep the cursor where it is while the surrounding structure is reindented, and the alt variants also tidy stray spaces.

Two options adjust this behaviour, and both default to off. The first, g:sexp_indent_does_clean, makes == and =- remove extra whitespace before indenting. The README's note is that if you enable it you may want to unmap alt-= and alt--, since those mappings become redundant. The second, g:sexp_indent_aligns_comments, makes == and =- also trigger re-alignment of end-of-line comments, which in turn makes the dedicated comment alignment mappings redundant, so the suggestion there is to unmap the LocalLeader a and LocalLeader A mappings instead. The dedicated comment commands align end-of-line comments in the current compound form or visual selection without moving the cursor.

The overall shape is that the plugin gives you a formatting pass that understands parentheses, and leaves the level of aggressiveness under your control through those two variables.

Changing mappings and the Treesitter fast path

Every default mapping can be changed through the g:sexp_mappings variable, so nothing is locked in if the default keys collide with your setup. There is a second escape hatch: every mapping is also exposed as a plug mapping, which the README says may be used for any purpose. Users who want a more explicit, opt-in configuration rather than a set of defaults are pointed at a separate help topic on explicit mappings, and the complete documentation lives under the vim-sexp help topic.

The optional performance path is Treesitter. The plugin does not require a Treesitter parser, but it will use one if it is available, and the README describes this as achieving significant performance gains. The help topic for it is short:

code
:help sexp-treesitter-support

This matters mostly on very large Lisp files, where repeatedly scanning for balanced brackets from the cursor position is the expensive part of structural editing. With a parser present, the plugin can jump directly to a boundary instead of walking to it. Neither the README nor the help topics cited above document how the plugin behaves when a parser is present but the file is not valid S-expression source, which is worth checking on your own files rather than assuming.

Limits of the plugin and the cost of adopting it

Several constraints are visible in the documentation rather than hidden. The plugin requires Vim 8.0 or later, so older Vim builds are out. It is Vim-specific, and although it runs under Neovim, some Neovim plugin managers need a different load approach, which the README acknowledges openly rather than papering over.

The dialect assumption is the sharpest edge. Macro characters default to Scheme's set when the file type is unknown, and the README notes that they differ by language. A dialect with a different reader, such as Common Lisp's distinct set of leading macro characters, is therefore not guaranteed to have its prefixes recognised until the relevant FileType is configured. Nothing in the documentation describes per-language configuration, so this is a real risk to test rather than a documented feature.

There is also no undo of the plugin's own behaviour, no refactoring, and no awareness of what a form means. The plugin operates on shape, not on semantics, so it cannot tell you that an argument list is malformed or that a form is dead code.

On maintenance, the repository is not archived and the last push was on 2026-10-01, so the project is being worked on. There are no GitHub releases, which means there is no tagged version to pin and no changelog to consult; you are tracking a branch, and the practical upgrade path is to re-read the help topics after an update rather than to read release notes. The licence is MIT, which is permissive; its only real obligation is to keep the copyright and licence notice when redistributing the plugin.

Editorial conclusion

vim-sexp earns its place for people who spend whole days inside parentheses: the text objects, the element model and the structure-aware indenting are the whole point of the plugin. It is a poor fit for editors that are not Vim 8.0 or later, for anyone unwilling to learn a second motion vocabulary, or for Lisp dialects whose reader syntax differs sharply from Scheme's, since macro characters default to Scheme's set in an unknown FileType. Verify first that your dialect's reader characters are recognised, that your plugin manager loads the plugin under Neovim, and that Treesitter is present if you expect the faster path.

Frequently asked questions

Does vim-sexp require Neovim to work?

No. The README lists Vim 8.0 or later as the requirement and says the traditional plugin load approach can also work under Neovim, though some Neovim-only plugin managers may require a different approach. An example Lazy.nvim configuration is provided in the help file, and the README suggests it may still be a useful starting point for other managers.

Can I repeat vim-sexp changes with the dot command?

Yes, if you install the optional vim-repeat plugin. The README states that vim-repeat enables use of the dot command for repeating change operations in vim-sexp, as well as repeating built-in operations that use vim-sexp's text objects. The plugin itself does not require vim-repeat.

How do I change the default vim-sexp key mappings?

The README says all default mappings can be changed via the g:sexp_mappings variable, and that every mapping is available as a plug mapping for any purpose. For a more explicit, opt-in configuration, the README points to the sexp-explicit-mappings help topic.

Is Treesitter required for vim-sexp?

No. The README states that although Treesitter is not a requirement, vim-sexp will use it to achieve significant performance gains if it is available. The topic is documented under the sexp-treesitter-support help entry.

Which Lisp dialects does vim-sexp understand macro characters for?

The README notes that macro characters differ by language and default to Scheme's macro character set in an unknown FileType. It does not document per-dialect configuration, so dialects with a different reader are worth testing directly before relying on element selection around prefixed data.

Official sources

  1. guns/vim-sexp on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/guns-vim-sexp.svg)](https://hysenlabs.com/projects/guns-vim-sexp)

Was this useful?