Fork me on GitHub

paredit?

Paredit allows structured navigation and editing of s-expressions.

Reference: Animated guide, Emacs Wiki, Ref card

Content:

This project

paredit.js allows general navigation and transformation of s-expressions, independent of a specific editor implementation. Pluck it into your editor of choice.

What?

Yes, it's just an interface.

  • An s-expression reader that produces a Lisp AST
  • A navigator that can be queried, e.g. for the start of the next expression
  • An editor that transforms code / ASTs, e.g. for splitting an expression, deleting expressions, etc.
  • The editor can also indent (ranges) of code

Demo

This shows an integration with the ace editor. The source code is here. The keybindings in the editor are just an example and can be redefined in any way imaginable. To get started I used most of the emacs/paredit bindings plus some more user friendly alternatives for basic operations.

Some of the short-cuts to try out:

  • Split Alt-Shift-s
  • Wrap around Alt-(, Alt-[, Alt-{
  • Indent Tab
  • Expand selection Ctrl-Shift-Space
  • Contract selection Ctrl-Alt-Space
  • ...

There will certainly be bugs, I'll be your biggest fan if you report them here!

; This editor keeps an AST in sync with the source code. Most commands that modify code
; will try to keep it syntactically correct. Movement / selection commands know the
; code entities and will use their boundaries. Code is indented according to typical
; lisp conventions.

(defn average
  "This is a comment. Move your cursor in here
  and press Ctrl-Shift-Space a couple of times.
  Try Ctrl-Alt-Space."
  [x y]
  (/ (+ x y) 2))

; select unindented code and press Tab
(map average
[1 2 3]
[4 5 6])

; Uncomment the next line, errors will be highlghted but note that
; there still is an AST usable for editing, even with syntax errors

; (broken code)))))

(un broken code (still
                 works :))

API

reader

  • parse(src, options)

High level parser, will produce an AST. Nodes have the form {start: NUMBER, end: NUMBER, type: STRING}. Listy entities will have open and close attributes. If options.addSourceForLeafs is truthy a source attribute will contain the raw code for leaf entities.

Example:

paredit.parse("(foo [bar baz])"); // =>
{
  start: 0, end: 15, errors: [], type: "toplevel",
  children: [{
    start: 0, end: 15, open: "(", close: ")", type: "list",
    children: [
      {start: 1, end: 4, type: "symbol"},
      {start: 5, end: 14, close: "]",open: "[", type: "list",
      children: [
        {start: 6, end: 9, type: "symbol"},
        {start: 10, end: 13, type: "symbol"}]
    }]
  }]
}
  • paredit.reader.readSeq(src, xform)

Low-level API. src is a string, xform(type, read, startPos, endPos, additionalArgs) an optional function called for every parsed entity.

Example:

paredit.reader.readSeq("(0 (1 2))");
// => [[0,[1,2]]]

A super simple "interpreter":

paredit.reader.readSeq("(+ 3 (- 4 2))", function(t, val, start, ehd) {
  return t === 'list' ?
    val.slice(2).reduce(function(sum,n) {
      return sum + (val[0] === '-' ? -n : n); }, val[1]) : val
}) // => 5

idx is the current position in the document. Thos functions return the index of the queries element:

  • paredit.navigator.forwardSexp(ast, idx)
  • paredit.navigator.forwardDownSexp(ast, idx)
  • paredit.navigator.backwardSexp(ast, idx)
  • paredit.navigator.backwardUpSexp(ast, idx)

Return [startIdx, endIdx]. Expansion is the range for the next containing element

  • paredit.navigator.rangeForDefun(ast, idx)
  • paredit.navigator.sexpRangeExpansion(ast, startIdx, endIdx)

paredit.walk offers lower level AST querying support:

  • paredit.walk.sexpsAt(ast,idx,matchFunc)
  • paredit.walk.containingSexpsAt(ast,idx,matchFunc)
  • paredit.walk.nextSexp(ast,idx,matchFunc)
  • paredit.walk.prevSexp(ast,idx,matchFunc)

editor

The editing functions return an object {changes: ARRAY, newIndex:NUMBER}. newIndex is where to put the cursor. Elements in changes are of the form: ['remove'|'insert', index, xxx]

Example:

  • ['remove', 3, 2] to delete two chars at index 3.
  • ['insert', 3, 'foo'] to insert "foo" at index 3.

Interface:

  • paredit.editor.wrapAround(ast,src,idx,wrapWithStart,wrapWithEnd,args)
  • paredit.editor.barfSexp(ast,src,idx,args)
  • paredit.editor.closeAndNewline(ast,src,idx,close)
  • paredit.editor.delete(ast,src,idx,args)
  • paredit.editor.indentRange(ast,src,start,end)
  • paredit.editor.killSexp(ast,src,idx,args)
  • paredit.editor.rewrite(ast,nodeToReplace,newNodes)
  • paredit.editor.slurpSexp(ast,src,idx,args)
  • paredit.editor.spliceSexp(ast,src,idx)
  • paredit.editor.splitSexp(ast,src,idx)

LICENSE

MIT