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