UNPKG

paredit.js

Version:

Generic parser and editor for s-expressions.

1,176 lines (981 loc) 43.7 kB
/*global window, process, global, module*/ "format global"; // If not on nodejs: concat or load lib files after loading this files. (function() { var isNodejs = typeof module !== "undefined" && module.exports, exports = isNodejs ? module.exports : (window.paredit = {}); if (isNodejs) { exports.reader = require("./lib/reader").reader; exports.navigator = require("./lib/navigator").navigator; exports.walk = require("./lib/navigator").walk; exports.editor = require("./lib/editor").editor; exports.specialForms = require("./lib/editor").specialForms; } exports.parse = function(src, options) { options = options || {}; var addSrc = options.hasOwnProperty('addSourceForLeafs') ? options.addSourceForLeafs : true; var errors = []; var nodes = exports.reader.readSeq(src, function xform(type, read, start, end, args) { var result = {type: type, start: start.idx, end: end.idx}; if (type === "error") { result.error = read.error; if (read.children) result.children = read.children; errors.push(result); } else if (addSrc && type !== 'list') result.source = src.slice(result.start, result.end); if (type === "list") result.children = read; if (type === "list" || type === "string" || (type === "error" && args)) { result.open = args.open; result.close = args.close; } return result; }); return { type: "toplevel", start: 0, end: (nodes && nodes.length && nodes[nodes.length-1].end) || 0, errors: errors, children: nodes }; }; })(); /*global window, process, global,module*/ "format global"; ;(function(run) { var isNodejs = typeof module !== "undefined" && module.exports; var exports = isNodejs ? module.exports : window.paredit; run(exports); })(function(exports) { var util = exports.util = { merge: function (objs) { if (arguments.length > 1) { return util.merge(Array.prototype.slice.call(arguments)); } if (Array.isArray(objs[0])) { // test for all? return Array.prototype.concat.apply([], objs); } return objs.reduce(function(merged, ea) { for (var name in ea) if (ea.hasOwnProperty(name)) merged[name] = ea[name]; return merged; }, {}); }, mapTree: function (treeNode, mapFunc, childGetter) { // Traverses the tree and creates a structurally identical tree but with // mapped nodes var mappedNodes = (childGetter(treeNode) || []).map(function(n) { return util.mapTree(n, mapFunc, childGetter); }) return mapFunc(treeNode, mappedNodes); }, flatFilterTree: function (treeNode, testFunc, childGetter) { // Traverses a `treeNode` recursively and returns all nodes for which // `testFunc` returns true. `childGetter` is a function to retrieve the // children from a node. var result = []; if (testFunc(treeNode)) result.push(treeNode); return result.concat( (childGetter(treeNode) || []).reduce(function(filtered, node) { return filtered.concat(util.flatFilterTree(node, testFunc, childGetter)); }, [])); }, last: function(a) { return a[a.length-1]; }, times: function(n, ch) { return new Array(n+1).join(ch); }, clone: function(obj) { // Shallow copy if (Array.isArray(obj)) return Array.prototype.slice.call(obj); var clone = {}; for (var name in obj) { clone[name] = obj[name]; } return clone; } } }); /*global window, process, global,module*/ "format global"; ;(function(run) { var isNodejs = typeof module !== "undefined" && module.exports; var exports = isNodejs ? module.exports : window.paredit; run(exports); })(function(exports) { exports.reader = { readSeq: function(src, xform) { return readSeq(null,src,Object.freeze([]),startPos(),xform).context; }, readSexp: function(src, xform) { return readSexp(null,src,Object.freeze([]),startPos(),xform).context[0]; } }; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // read logic var eosexp = {}, eoinput = {}, // flags close = {'[': ']', '(': ')', '{': '}'}, opening = Object.keys(close), closing = opening.map(function(k) { return close[k]; }), symRe = /[^\s\[\]\(\)\{\},"\\`@^#~]/, readerSpecials = /[`@^#~]/; function readSexp(contextStart, input, context, pos, xform) { var ch = input[0]; // We have reached the end of input but expting more. if (!ch && contextStart && close[contextStart]) { return {input: input, context: context, pos: pos, flag: eoinput}; } // If there is no contextStart and no char left we are at the topLevel // and done reading. if (!ch && !close[contextStart]) return { input: input, context: context, pos: pos, flag: eoinput } // 3. whitespace if (/\s|,/.test(ch)) return { input: input.slice(1), context: context, pos: forward(pos, ch) }; // 4. Various read rules if (readerSpecials.test(ch)) return readReaderSpecials(input, context, pos, xform); if (ch === ';') return readComment(input, context, pos, xform); if (ch === '"') return readString(input, context, pos, xform); if (ch === '\\') return readChar(input, context, pos, xform); if (/[0-9]/.test(ch)) return readNumber(input, context, pos, xform); if (ch === '-' && (/[0-9]/.test(input[1]) || (input[1] == '.' && /[0-9]/.test(input[2])))) return readNumber(input, context, pos, xform); if (ch === '.' && (/[0-9]/.test(input[1]))) return readNumber(input, context, pos, xform); if (symRe.test(ch)) return readSymbol(input, context, pos, xform); // 5. list end? if (closing.indexOf(ch) > -1) { if (!contextStart) { var junk = readJunk(input, context, pos, xform); return {input: junk.input, context: junk.context, pos: junk.pos} } return {input: input, context: context, pos: pos, flag: eosexp} } // 6. list start? if (opening.indexOf(ch) > -1) { var startPos = clonePos(pos), nested = readSeq(ch, input.slice(1), Object.freeze([]), forward(pos, ch), xform), nextCh = nested.input[0], brackets = {open: ch, close: close[ch]}; var sexp, endPos; if (nextCh !== close[ch]) { var errPos = clonePos(nested.pos), errMsg = "Expected '" + close[ch] + "'" + (nextCh ? " but got '" + nextCh + "'" : " but reached end of input"), children = nested.context, err = readError(errMsg, startPos, errPos, children); sexp = callTransform(xform, "error", err, startPos, errPos, brackets); endPos = nextCh ? forward(nested.pos, nextCh) : nested.pos; } else { endPos = nextCh ? forward(nested.pos, nextCh) : nested.pos; sexp = callTransform(xform, "list", nested.context, startPos, endPos, brackets); } context = context.concat([sexp]); var restInput = nested.input.slice(nextCh ? 1 : 0); return {input: restInput, context: context, pos: endPos} } // If we are here, either there is a char not covered by the sexp reader // rules or we are toplevel and encountered garbage var startPos = clonePos(pos), errPos = forward(pos, ch); var err = readError("Unexpected character: " + ch, startPos, errPos, null); err = callTransform(xform, "error", err, startPos, errPos); context = context.concat([err]); return {input: input.slice(1), context: context, pos: errPos}; } function readSeq(contextStart, input, context, pos, xform) { var result, counter = 0; while (true) { var startRow = pos.row, startCol = pos.column; result = readSexp(contextStart, input, context, pos, xform); input = result.input; context = result.context; pos = result.pos; var endReached = result.flag === eoinput || (result.flag === eosexp && (contextStart || !input.length)); if (!endReached && pos.row <= startRow && pos.column <= startCol) throw new Error("paredit reader cannot go forward at " + printPos(pos) + " with input " + input); if (endReached) break; // if (result.flag === eosexp && !contextStart) // result = readJunk(input, context, pos, xform); // input = result.input; context = result.context; pos = result.pos; }; return {input: input, context: context, pos: pos}; } function readString(input, context, pos, xform) { var escaped = false; var startPos = clonePos(pos); var string = input[0]; pos = forward(pos, input[0]); input = input.slice(1); return takeWhile(input, pos, function(c) { if (!escaped && c === '"') return false; if (escaped) escaped = false else if (c === "\\") escaped = true; return true; }, function(read, rest, prevPos, newPos) { var result; if (rest[0] == '"') { string = string + read + rest[0]; newPos = forward(newPos, rest[0]); rest = rest.slice(1); result = callTransform(xform, "string", string, startPos, newPos, {open: '"', close: '"'}); } else { var err = readError("Expected '\"' but reached end of input", startPos, newPos, null); result = callTransform(xform, "error", err, prevPos, newPos); } context = context.concat([result]); return {pos:newPos,input:rest,context:context}; }); } function readChar(input, context, pos, xform) { // char like \x var prevPos = clonePos(pos), read = input.slice(0,2), newPos = forward(pos, read), result = callTransform(xform, "char", read, prevPos, newPos), rest = input.slice(2); context = context.concat([result]); return {pos:newPos, input:rest, context: context}; } function readSymbol(input, context, pos, xform) { return takeWhile(input, pos, function(c) { return symRe.test(c); }, function(read, rest, prevPos, newPos) { var result = callTransform(xform, "symbol", read, prevPos, newPos); context = context.concat([result]); return {pos: newPos,input:rest,context:context}; }); } function readNumber(input, context, pos, xform) { var first = true, seenSeperator = false; return takeWhile(input, pos, function(c) { if (first) { first = false; if (c === '-') return true; } if(seenSeperator && c === '.') { seenSeperator = false; return true } return /[0-9.]/.test(c); }, function(read, rest, prevPos, newPos) { var result = callTransform(xform, "number", Number(read), prevPos, newPos); context = context.concat([result]) return {pos:newPos,input:rest,context:context}; }); } function readComment(input, context, pos, xform) { var prevPos = clonePos(pos), comment = "", rest = input; while (rest.length && /^\s*;/.test(rest)) { var read = readline(rest); comment += read[0]; rest = read[1]; } var newPos = forward(pos, comment), result = callTransform(xform, "comment", comment, prevPos, newPos); context = context.concat([result]); return {pos: newPos, input:rest, context:context}; } function readReaderSpecials(input, context, pos, xform) { var prevPos = clonePos(pos), read = input.slice(0,1), newPos = forward(pos, read), result = callTransform(xform, "special", read, prevPos, newPos), rest = input.slice(1); context = context.concat([result]); return {pos:newPos, input:rest, context: context}; } function readJunk(input, context, pos, xform) { return takeWhile(input, pos, // FIXME: there can be other junk except closing parens... function(c) { return closing.indexOf(c) > -1; }, function(read, rest, prevPos, newPos) { var err = readError("Unexpected input: '" + read + "'", prevPos, newPos, null); var result = callTransform(xform, "error", err, prevPos, newPos); context = context.concat([result]); return {pos: newPos,input:rest,context:context}; }); } // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- function readError(msg, startPos, endPos, children) { return { error: msg + " at line " + (endPos.row+1) + " column " + endPos.column, start: clonePos(startPos), end: clonePos(endPos), children: children } } function callTransform(xform, type, read, start, end, args) { return xform ? xform(type, read, clonePos(start), clonePos(end), args) : read; } function takeWhile(string, pos, fun, withResultDo) { var startPos = clonePos(pos), result = ""; for (var i = 0; i < string.length; i++) { if (fun(string[i])) result += string[i]; else break; } return withResultDo( result, string.slice(result.length), startPos, forward(pos, result)); } // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // position helpers function startPos() { return {idx: 0, column: 0, row: 0}; } function clonePos(pos) { return {idx: pos.idx, column: pos.column, row: pos.row}; } function printPos(pos) { return JSON.stringify(pos); } function readline(input) { var endIdx = input.indexOf("\n"); endIdx = endIdx > -1 ? endIdx+1 : input.length; var read = input.slice(0, endIdx); var rest = input.slice(endIdx); return [read, rest]; } function forward(pos, read) { // note: pos is deliberately transient for performance if (!read) return pos; pos.idx += read.length; var lines = read.split("\n"); var ll = lines.length; pos.row += ll-1; var lastRowL = lines[ll-1].length; pos.column = ll > 1 ? lastRowL : pos.column + lastRowL; return pos; } }); /*global window, process, global,module*/ "format global"; ;(function(run) { var isNodejs = typeof module !== "undefined" && module.exports; var exports = isNodejs ? module.exports : window.paredit; var util = isNodejs ? require('./util').util : window.paredit.util; run(util, exports); })(function(util, exports) { function last(a) { return a[a.length-1]; }; var nav = exports.navigator = { forwardSexp: function(ast, idx) { var current = last(w.containingSexpsAt(ast,idx, function(n) { return !w.hasChildren(n); })); if (current) return current.end; var next = w.nextSexp(ast, idx); return next ? next.end : idx; }, backwardSexp: function(ast, idx) { var current = last(w.containingSexpsAt(ast,idx, function(n) { return !w.hasChildren(n); })); if (current) return current.start; var prev = w.prevSexp(ast, idx); return prev ? prev.start : idx; }, forwardDownSexp: function(ast, idx) { var next = w.nextSexp(ast, idx, function(n) { return n.type === 'list'}); if (!next) return idx; if (next.children && next.children[0]) return next.children[0].start; return next.start + 1; }, backwardUpSexp: function(ast, idx) { var containing = w.containingSexpsAt(ast, idx, function(n) { return n.type === 'list' || n.type === 'string' || n.type === 'comment'; }); if (!containing || !containing.length) return idx; return last(containing).start; }, closeList: function(ast, idx) { var containing = w.containingSexpsAt(ast, idx); var l = last(containing); if (!l || l.type === "toplevel") return idx; if (l.type === "string" || l.type === "comment") return undefined; var lists = containing.filter(w.hasChildren); return last(lists).end; }, sexpRange: function(ast, idx) { // finds the range of the sexp at idx return nav.sexpRangeExpansion(ast, idx, idx); }, sexpRangeExpansion: function(ast, startIdx, endIdx) { // startIdx, endIdx define a range. Return the range of the next // enclosing sexp. // If we have another non-list entity directly to our left or right like // in @*xxx* we select that if (startIdx !== endIdx) { // find the entity already selected... var directMatchedStart = last(w.sexpsAt(ast, startIdx, function(n) { return n.start === startIdx; })); var directMatchedEnd = directMatchedStart && last(w.sexpsAt(ast, endIdx, function(n) { return n.end === endIdx; })); if (directMatchedStart && directMatchedEnd) { var directLeft = last(w.sexpsAt(ast, startIdx, function(n) { return n.start < startIdx && !w.hasChildren(n); })); if (directLeft) return [directLeft.start, endIdx]; var directRight = last(w.sexpsAt(ast, endIdx, function(n) { return endIdx < n.end && !w.hasChildren(n); })); if (directRight) return [startIdx, directRight.end]; } } var sexp = last(util.flatFilterTree(ast, function(n) { if (n.type === 'toplevel') return false; if (startIdx === endIdx) return n.start <= startIdx && endIdx <= n.end; if (n.start === startIdx) return endIdx < n.end; if (n.end === endIdx) return n.start < startIdx; return n.start < startIdx && endIdx < n.end; }, getChildren)); if (!sexp) return null; var isBorderSel = sexp.start === startIdx || sexp.end === endIdx; if (sexp.type === 'list' || sexp.type === 'string') { if (isBorderSel && (startIdx === sexp.start || endIdx === sexp.end)) return [sexp.start, sexp.end]; if (sexp.start+1 < startIdx || endIdx < sexp.end-1) return [sexp.start+1, sexp.end-1] } return [sexp.start, sexp.end]; }, rangeForDefun: function(ast, idx) { var node = ast.children && ast.children.filter(function(n) { return n.start <= idx && idx <= n.end; })[0]; return node ? [node.start, node.end] : null; } }; var w = exports.walk = { hasChildren: function(n) { return n.type === 'list' || n.type === 'toplevel' || (n.type === 'error' && n.children); }, containingSexpsAt: function(ast, idx, matchFunc) { return util.flatFilterTree(ast, function(n) { return (n.type === 'toplevel' || (n.type === 'error' && n.start < idx && idx <= n.end) || (n.start < idx && idx < n.end)) && (!matchFunc || matchFunc(n)); }, getChildren); }, sexpsAt: function(ast, idx, matchFunc) { return util.flatFilterTree(ast, function(n) { return n.start <= idx && idx <= n.end && (!matchFunc || matchFunc(n)); }, getChildren); }, nextSexp: function(ast, idx, matchFunc) { // Find the next sexp following idx. If idx directly points to a list start, // the list it is. Otherwise get the containing list and find the closest // following children sexp. var listsAt = util.flatFilterTree(ast, function(n) { return n.start <= idx && idx < n.end && w.hasChildren(n); }, getChildren); if (!listsAt.length) return null; var direct = listsAt.filter(function(n) { return n.start === idx && n.type !== 'toplevel'; })[0]; if (direct) return direct; var list = last(listsAt).children.filter(function(n) { return idx <= n.start && (!matchFunc || !!matchFunc(n)); }) if (list.length) return list[0]; return null; }, prevSexp: function(ast,idx,matchFunc) { var listsAt = util.flatFilterTree(ast, function(n) { return n.start < idx && idx <= n.end && w.hasChildren(n); }, getChildren); if (!listsAt.length) return null; var direct = listsAt.filter(function(n) { return n.end === idx && n.type !== 'toplevel';; })[0]; if (direct) return direct; var list = last(listsAt).children.filter(function(n) { return n.end <= idx && (!matchFunc || !!matchFunc(n)); }) if (list.length) return last(list); return null; }, stringify: function(node) { return util.mapTree(node, function(n, children) { if (n.type === 'list' || n.type === 'toplevel') return '(' + children.join(" ") + ')'; return n.source ? n.source : util.times(node.end-node.start, 'x'); }, function(n) { return (n && n.children) || []; }); }, source: function(src, node) { return node.source ? node.source : src.slice(node.start,node.end); } } function getChildren(node) { return node.children || []; } }); /*global window, process, global,module*/ "format global"; ;(function(run) { var isNodejs = typeof module !== "undefined" && module.exports; var exports = isNodejs ? module.exports : window.paredit; var util = isNodejs ? require('./util').util : window.paredit.util; var nav = isNodejs ? require("./navigator").navigator : window.paredit.navigator; var w = isNodejs ? require("./navigator").walk : window.paredit.walk; run(nav, w, util, exports); })(function(nav, w, util, exports) { // (map (comp name first) (seq clojure.lang.Compiler/specials)) exports.specialForms = [ "&", "monitor-exit", /^case/, "try", /^reify/, "finally", /^(.*-)?loop/, /^do/, /^let/, /^import/, "new", /^deftype/, /^let/, "fn", "recur", /^set.*!$/, ".", "var", "quote", "catch", "throw", "monitor-enter", 'ns', 'in-ns', /^([^\/]+\/)?def/,/^if/,/^when/,/^unless/,/->/, "while", "for", /(^|\/)with/, "testing", "while", "cond", "condp", "apply", "binding", "locking", "proxy", "reify", /^extend/, // midje "facts"]; var ed = exports.editor = { rewrite: function(ast, nodeToReplace, newNodes) { var indexOffset = newNodes.length ? last(newNodes).end - nodeToReplace.end : nodeToReplace.start - nodeToReplace.end; var parents = w.containingSexpsAt(ast, nodeToReplace.start); // Starting from the parent of the nodeToReplace: construct new // parents out of the changed child, bottom up // With that we don't need to modify the exising AST. Note that during // this recursive construction we need to update the children to the "right" // of the modification var replaced = parents.reduceRight(function(replacement, parent) { var idxInParent = parent.children.indexOf(replacement.original); var childList; if (idxInParent > -1) { childList = parent.children.slice(0,idxInParent) .concat(replacement.nodes) .concat(parent.children.slice(idxInParent+1) .map(moveNode.bind(null,indexOffset))); } else childList = parent.children; var newParent = util.merge(parent, { end: parent.end+indexOffset, children: childList }); return {original: parent, nodes: [newParent]}; }, {original: nodeToReplace, nodes: newNodes}); return replaced.nodes[0]; }, openList: function(ast, src, idx, args) { args = args || {}; var count = args.count || 1; var open = args.open || '(', close = args.close || ')'; if (args.freeEdits || ast.errors && ast.errors.length) return { changes: [["insert", idx, open]], newIndex: idx+open.length } var containing = w.containingSexpsAt(ast, idx); var l = last(containing); if (l && l.type === "comment" || l.type === "string") return {changes: [["insert", idx, open]], newIndex: idx+open.length} if (!args.endIdx) { // not a selection range return {changes: [["insert", idx, open+close]], newIndex: idx+open.length} } var parentStart = last(w.containingSexpsAt(ast, idx, w.hasChildren)); var parentEnd = last(w.containingSexpsAt(ast, args.endIdx, w.hasChildren)); // does selection span multiple expressions? collapse selection // var left = parentEnd.children.filter(function(ea) { return ea.end <= pos; }); // var right = parentEnd.children.filter(function(ea) { return pos <= ea.start; }); if (parentStart !== parentEnd) { return {changes: [["insert", idx, open+close]], newIndex: idx+open.length} } var inStart = parentEnd.children.filter(function(ea) { return ea.start < idx && idx < ea.end ; }), inEnd = parentEnd.children.filter(function(ea) { return ea.start < args.endIdx && args.endIdx < ea.end ; }), moveStart = inStart[0] && inStart[0] !== inEnd[0] && (inEnd[0] || inStart[0].type !== 'symbol'), moveEnd = inEnd[0] && inStart[0] !== inEnd[0] && (inStart[0] || inEnd[0].type !== 'symbol'), insertOpenAt = moveStart ? inStart[0].end : idx, insertCloseAt = moveEnd ? inEnd[0].start : args.endIdx; return { changes: [["insert", insertCloseAt, close], ["insert", insertOpenAt, open]], newIndex: insertOpenAt+open.length }; }, spliceSexp: function(ast, src, idx) { var sexps = w.containingSexpsAt(ast,idx,w.hasChildren); if (!sexps.length) return null; var parent = sexps.pop(); var onTop = parent.type === "toplevel"; var insideSexp = parent.children.filter(function(n) { return n.start < idx && idx < n.end; })[0]; var insideString = insideSexp && insideSexp.type === 'string'; var changes = [], newIndex = idx; if (!onTop) changes.push(['remove', parent.end-1, parent.close.length]); if (insideString) { changes.push(['remove', insideSexp.end-1, insideSexp.close.length]); changes.push(['remove', insideSexp.start, insideSexp.open.length]); newIndex -= insideSexp.open.length; } if (!onTop) { changes.push(['remove', parent.start, parent.open.length]); newIndex -= parent.open.length; } return {changes: changes, newIndex: newIndex}; }, spliceSexpKill: function(ast, src, idx, args) { args = args || {} var count = args.count || 1; var backward = args.backward; var sexps = w.containingSexpsAt(ast,idx,w.hasChildren); if (!sexps.length) return null; if (backward) { var left = leftSiblings(last(sexps), idx); var killed = ed.killSexp(ast, src, idx/*last(left).end*/, {count: left.length, backward: true}) } else { var right = rightSiblings(last(sexps), idx); var killed = ed.killSexp(ast, src, idx/*last(right).end*/, {count: right.length, backward: false}) } var spliced = ed.spliceSexp(ast,src,idx); if (!killed) return spliced; if (!spliced) return killed; var changes = Array.prototype.slice.call(spliced.changes); if (changes.length === 2) changes.splice(1,0,killed.changes[0]) else if (changes.length === 4) changes.splice(2,0,killed.changes[0]) return { changes: changes, newIndex: killed.newIndex-(changes.length === 3 ? 1 : 2) } }, splitSexp: function(ast, src, idx) { var sexps = w.containingSexpsAt(ast,idx); if (!sexps.length) return null; var sexp = sexps.pop(); if (sexp.type === "toplevel") return null; if (!w.hasChildren(sexp) && sexp.type !== "string") return null; // we are dealing with a list or string split var insertion = sexp.close+" "+sexp.open, newIndex = idx+sexp.close.length, changes = [['insert', idx, insertion]]; return {changes: changes, newIndex: newIndex}; }, killSexp: function(ast, src, idx, args) { args = args || {} var count = args.count || 1; var backward = args.backward; var sexps = w.containingSexpsAt(ast,idx, w.hasChildren); if (!sexps.length) return null; var parent = sexps.pop(); var insideSexp = parent.children.filter(function(n) { return n.start < idx && idx < n.end; })[0]; if (insideSexp) { var from = backward ? insideSexp.start : idx; var to = backward ? idx : insideSexp.end; if (insideSexp.type === 'string') { from += backward ? insideSexp.open.length : 0; to += backward ? 0 : -insideSexp.close.length; } return { changes: [['remove', from, to-from]], newIndex: from } } if (insideSexp && insideSexp.type === 'string') { var from = backward ? insideSexp.start+insideSexp.open.length : idx; var to = backward ? idx : insideSexp.end-insideSexp.close.length; return { changes: [['remove', from, to-from]], newIndex: from } } if (backward) { var left = leftSiblings(parent, idx); if (!left.length) return null; var remStart = left.slice(-count)[0].start; var changes = [['remove', remStart, idx-remStart]]; var newIndex = remStart; } else { var right = rightSiblings(parent, idx); if (!right.length) return null; var newIndex = idx; var changes = [['remove', idx, last(right.slice(0,count)).end-idx]]; } return {changes: changes, newIndex: newIndex}; }, wrapAround: function(ast, src, idx, wrapWithStart, wrapWithEnd, args) { var count = (args && args.count) || 1; var sexps = w.containingSexpsAt(ast,idx, w.hasChildren); if (!sexps.length) return null; var parent = last(sexps); var sexpsToWrap = parent.children.filter(function(c) { return c.start >= idx; }).slice(0,count); var end = last(sexpsToWrap); var changes = [ ['insert', idx, wrapWithStart], ['insert', (end ? end.end : idx) + wrapWithStart.length, wrapWithEnd]]; return {changes: changes, newIndex: idx+wrapWithStart.length}; }, closeAndNewline: function(ast, src, idx, close) { close = close || ")" var sexps = w.containingSexpsAt(ast,idx, function(n) { return w.hasChildren(n) && n.close === close; }); if (!sexps.length) return null; var parent = last(sexps), newlineIndent = times(rowColumnOfIndex(src, parent.start), ' '), insertion = "\n"+newlineIndent; var changes = [ ['insert', parent.end, insertion]]; return {changes: changes, newIndex: parent.end+insertion.length}; }, barfSexp: function(ast, src, idx, args) { var backward = args && args.backward; var sexps = w.containingSexpsAt(ast,idx, w.hasChildren); if (!sexps.length) return null; var parent = last(sexps), inner = last(w.containingSexpsAt(ast,idx)); if (inner === parent) inner = null; if (backward) { var left = leftSiblings(parent, idx); if (!left.length) return null; var changes = [ ['insert', left[1] ? left[1].start : (inner ? inner.start : idx), parent.open], ['remove', parent.start, parent.open.length]]; } else { var right = rightSiblings(parent, idx); if (!right.length) return null; var changes = [ ['remove', parent.end-parent.close.length, parent.close.length], ['insert', right[right.length-2] ? right[right.length-2].end : (inner ? inner.end : idx), parent.close]]; } return {changes: changes, newIndex: idx}; }, slurpSexp: function(ast, src, idx, args) { var backward = args && args.backward; var count = args.count || 1; var sexps = w.containingSexpsAt(ast,idx, w.hasChildren); if (sexps.length < 2) return null; var parent = sexps.pop(); var parentParent = sexps.pop(); if (backward) { var left = leftSiblings(parentParent, idx); if (!left.length) return null; var changes = [ ['remove', parent.start, parent.open.length], ['insert', left.slice(-count)[0].start, parent.open]]; } else { var right = rightSiblings(parentParent, idx); if (!right.length) return null; var changes = [ ['insert', last(right.slice(0,count)).end, parent.close], ['remove', parent.end-parent.close.length, parent.close.length]]; } return {changes: changes, newIndex: idx}; }, transpose: function(ast,src,idx,args) { args = args || {}; var outerSexps = w.containingSexpsAt(ast, idx, w.hasChildren), parent = last(outerSexps), left = leftSiblings(parent, idx), right = rightSiblings(parent, idx), inside = parent.children.find(function(n) { return n.start < idx && idx < n.end; }); // if "inside" a leaf node, use it to transpose with node left of in if (inside) right = [inside]; // nothing there to transpose... if (!left.length || !right.length) return null; var l = last(left), r = right[0], insertion = src.slice(l.end, r.start) + w.source(src, l); return { changes: [ ['insert', r.end, insertion], ['remove', l.start, r.start-l.start]], newIndex: idx - (l.end-l.start)+(r.end-r.start) }; }, delete: function(ast,src,idx,args) { args = args || {}; var count = args.count || 1, backward = !!args.backward, endIdx = args.endIdx; // for text ranges if (args.freeEdits || ast.errors && ast.errors.length) { return endIdx ? { changes: [["remove", idx, endIdx-idx]], newIndex: idx } : { changes: [["remove", backward ? idx-count : idx, count]], newIndex: backward ? idx-count : idx } } var outerSexps = w.containingSexpsAt(ast, idx), outerLists = outerSexps.filter(function(n) { return w.hasChildren(n); }), parent = last(outerLists), sexp = last(outerSexps); var deleteRange = typeof endIdx === "number"; if (deleteRange) { var endParent = last(w.containingSexpsAt(ast, endIdx, w.hasChildren)); if (parent !== endParent) return null; var insideNodeStart = last(w.sexpsAt(parent, idx)); var insideNodeEnd = last(w.sexpsAt(parent, endIdx)); // don't delete only one " of strings var atStartOfUnsaveDelete = !isSaveToPartialDelete(insideNodeStart) && insideNodeStart.start === idx; var atEndOfUnsaveDelete = !isSaveToPartialDelete(insideNodeEnd) && insideNodeEnd.end === endIdx; if (insideNodeStart === insideNodeEnd && ((atStartOfUnsaveDelete && !atEndOfUnsaveDelete) || (!atStartOfUnsaveDelete && atEndOfUnsaveDelete))) return null; // if (!isSaveToPartialDelete(insideNodeStart) && insideNodeStart.start === idx) return null; // if (!isSaveToPartialDelete(insideNodeEnd) && insideNodeEnd.end === endIdx) return null; if (((insideNodeEnd !== parent && !isSaveToPartialDelete(insideNodeEnd) && !atEndOfUnsaveDelete) || (insideNodeStart !== parent && !isSaveToPartialDelete(insideNodeStart) && !atStartOfUnsaveDelete)) && insideNodeStart !== insideNodeEnd) return null; if ((parent.children.indexOf(insideNodeStart) === -1 && insideNodeStart !== parent) || (parent.children.indexOf(insideNodeEnd) === -1 && insideNodeEnd !== parent)) return null; var delStart = Math.min(idx, endIdx), delEnd = Math.max(idx, endIdx); return {changes: [['remove', delStart, delEnd-delStart]], newIndex: delStart} } var isInList = parent === sexp, left = isInList && leftSiblings(parent, idx), right = isInList && rightSiblings(parent, idx), noDelete = {changes: [], newIndex: idx}, moveLeft = {changes: [], newIndex: idx-1}, simpleDelete = { changes: [['remove', backward ? idx-count : idx, count]], newIndex: backward ? idx-count : idx }, changes = [], newIndex = idx; if (!isInList && sexp.type === 'comment') return simpleDelete; if (left && left.length && backward) { var n = last(left); if (n.end !== idx || isSaveToPartialDelete(n)) return simpleDelete; if (isEmpty(n) || n.type === "char") return deleteSexp(n); if (count == 1) return moveLeft; return noDelete; } if (right && right.length && !backward) { var n = right[0]; if (n.start !== idx || isSaveToPartialDelete(n)) return simpleDelete; if (isEmpty(n) || n.type === "char") return deleteSexp(n); return noDelete; } if (!isInList) parent = sexp; var atStart = idx === parent.start+(parent.open ? parent.open.length : 0); var atEnd = idx === parent.end-(parent.close ? parent.close.length : 0); if ((!parent.children || !parent.children.length) && ((atStart && backward) || (atEnd && !backward))) { return deleteSexp(parent); } if (atStart && backward && (isInList ? parent.children.length : parent.end-parent.start > 1)) return noDelete; if (atEnd && !backward && (isInList ? parent.children.length : parent.end-parent.start > 1)) return noDelete; return simpleDelete; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- function isEmpty(sexp) { return (sexp.type === 'string' || sexp.type === "list") && sexp.end-sexp.start === sexp.open.length+sexp.close.length } function deleteSexp(sexp) { return { changes: [['remove', sexp.start, sexp.end-sexp.start]], newIndex: sexp.start} } function isSaveToPartialDelete(n) { return n.type === 'symbol'|| n.type === 'comment' || n.type === 'number' || n.type === 'special'; } }, indentRange: function(ast, src, start, end) { var startLineIdx = rowStartIndex(src, start), endLineIdx = src.slice(end).indexOf("\n"); endLineIdx = endLineIdx > -1 ? endLineIdx+end : src.length; var linesToIndent = src.slice(startLineIdx, endLineIdx).split("\n"); return linesToIndent.reduce(function(indent, line) { var idx = indent.idx, changes = indent.changes, ast = indent.ast, src = indent.src; var outerSexps = w.containingSexpsAt(ast, idx, w.hasChildren), parent = last(outerSexps), sexpAtBol = parent && last(w.sexpsAt(ast, idx)); if (!parent) return { idx: idx+line.length+1, newIndex: idx, changes:changes, ast:ast, src: src }; // whitespace at bol that needs to be "removed" var ws = line.match(/^\s*/)[0], // figure out much whitespace we need to add indentOffset = sexpAtBol && sexpAtBol.type === 'string' && idx > sexpAtBol.start ? 0 : computeIndentOffset(src, parent, idx) - ws.length, lineLength = line.length + indentOffset; // record what needs to be changed and update source if (indentOffset > 0) { var insert = times(indentOffset, " "); changes.push(["insert", idx, insert]); src = src.slice(0,idx) + insert + src.slice(idx); } else if (indentOffset < 0) { changes.push(["remove", idx, -indentOffset]); src = src.slice(0,idx) + src.slice(idx-indentOffset); } // also update the ast: "move" the next node to the right accordingly, // "update" the entire ast var right = rightSiblings(parent, idx)[0]; if (right) { var indentedRight = moveNode(indentOffset, right); ast = ed.rewrite(ast, right, [indentedRight]); } else { // if no siblings, udpdate the end of the list node ast = ed.rewrite(ast, parent, [util.merge(parent, {end: parent.end+indentOffset})]); } return { idx: idx + lineLength + 1, /*newline*/ newIndex: idx + indentOffset, // for cursor placement changes: changes, ast: ast, src: src } }, {idx: startLineIdx, changes: [], ast: ast, src: src}); } }; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // positioning helpers function rowStartIndex(src, idx) { return src.slice(0,idx).lastIndexOf("\n")+1; } function rowColumnOfIndex(src, idx) { return idx - rowStartIndex(src,idx); } function computeIndentOffset(src, parentSexp, idx) { if (parentSexp.type === 'toplevel') return 0; var left = leftSiblings(parentSexp, idx); if (isSpecialForm(parentSexp, src)) return rowColumnOfIndex(src, parentSexp.start + parentSexp.open.length+1); if (left.length <= 1 || parentSexp.open !== "(") return rowColumnOfIndex(src, parentSexp.start + parentSexp.open.length); return rowColumnOfIndex(src, left[1].start); } // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // lang helper function last(a) { return a[a.length-1]; }; function times(n, ch) { return new Array(n+1).join(ch); } // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // ast helpers function moveNode(offset, n) { // changes start/end of node and its children return util.mapTree(n, function(n, children) { return util.merge(n, { start: n.start+offset, end: n.end+offset, children: children }); }, function(n) { return n.children; }); } function leftSiblings(parentNode, idx) { return parentNode.children.filter(function(n) { return n.end <= idx; }); } function rightSiblings(parentNode, idx) { return parentNode.children.filter(function(n) { return idx <= n.start; }); } function isSpecialForm(parentSexp, src) { if (!w.hasChildren(parentSexp) || !parentSexp.children.length) return false; var srcOfFirstNode = parentSexp.children[0].source; if (!srcOfFirstNode) return false; return exports.specialForms.some(function(f) { if (typeof f === "string") return f === srcOfFirstNode; else if (typeof f === "function") return f(srcOfFirstNode, parentSexp.children[0]); else if (f instanceof RegExp) return f.test(srcOfFirstNode); else return false; }); } });