/* ***** BEGIN LICENSE BLOCK ***** * Distributed under the BSD license: * * Copyright (c) 2010, Ajax.org B.V. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Ajax.org B.V. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ define(function(require, exports, module) { "use strict"; /** * This object maintains the undo stack for an [[EditSession `EditSession`]]. * @class UndoManager **/ /** * Resets the current undo state and creates a new `UndoManager`. * * @constructor **/ var UndoManager = function() { this.$maxRev = 0; this.$fromUndo = false; this.reset(); }; (function() { this.addSession = function(session) { this.$session = session; }; /** * Provides a means for implementing your own undo manager. `options` has one property, `args`, an [[Array `Array`]], with two elements: * * - `args[0]` is an array of deltas * - `args[1]` is the document to associate with * * @param {Object} options Contains additional properties * **/ this.add = function(delta, allowMerge, session) { if (this.$fromUndo) return; if (delta == this.$lastDelta) return; if (allowMerge === false || !this.lastDeltas) { this.lastDeltas = []; this.$undoStack.push(this.lastDeltas); delta.id = this.$rev = ++this.$maxRev; } if (delta.action == "remove" || delta.action == "insert") this.$lastDelta = delta; this.lastDeltas.push(delta); }; this.addSelection = function(selection, rev) { this.selections.push({ value: selection, rev: rev || this.$rev }); }; this.startNewGroup = function() { this.lastDeltas = null; return this.$rev; }; this.markIgnored = function(from, to) { if (to == null) to = this.$rev + 1; var stack = this.$undoStack; for (var i = stack.length; i--;) { var delta = stack[i][0]; if (delta.id <= from) break; if (delta.id < to) delta.ignore = true; } this.lastDeltas = null; }; this.getSelection = function(rev, after) { var stack = this.selections; for (var i = stack.length; i--;) { var selection = stack[i]; if (selection.rev < rev) { if (after) selection = stack[i + 1]; return selection; } } }; this.getRevision = function() { return this.$rev; }; this.getDeltas = function(from, to) { if (to == null) to = this.$rev + 1; var stack = this.$undoStack; var end = null, start = 0; for (var i = stack.length; i--;) { var delta = stack[i][0]; if (delta.id < to && !end) end = i+1; if (delta.id <= from) { start = i + 1; break; } } return stack.slice(start, end); }; this.getChangedRanges = function(from, to) { if (to == null) to = this.$rev + 1; }; this.getChangedLines = function(from, to) { if (to == null) to = this.$rev + 1; }; /** * [Perform an undo operation on the document, reverting the last change.]{: #UndoManager.undo} * @param {Boolean} dontSelect {:dontSelect} * * @returns {Range} The range of the undo. **/ this.undo = function(session, dontSelect) { this.lastDeltas = null; var stack = this.$undoStack; if (!rearrangeUndoStack(stack, stack.length)) return; if (!session) session = this.$session; if (this.$redoStackBaseRev !== this.$rev && this.$redoStack.length) this.$redoStack = []; this.$fromUndo = true; var deltaSet = stack.pop(); var undoSelectionRange = null; if (deltaSet && deltaSet.length) { undoSelectionRange = session.undoChanges(deltaSet, dontSelect); this.$redoStack.push(deltaSet); this.$syncRev(); } this.$fromUndo = false; return undoSelectionRange; }; /** * [Perform a redo operation on the document, reimplementing the last change.]{: #UndoManager.redo} * @param {Boolean} dontSelect {:dontSelect} * **/ this.redo = function(session, dontSelect) { this.lastDeltas = null; if (!session) session = this.$session; this.$fromUndo = true; if (this.$redoStackBaseRev != this.$rev) { var diff = this.getDeltas(this.$redoStackBaseRev, this.$rev + 1); rebaseRedoStack(this.$redoStack, diff); this.$redoStackBaseRev = this.$rev; this.$redoStack.forEach(function(x) { x[0].id = ++this.$maxRev; }, this); } var deltaSet = this.$redoStack.pop(); var redoSelectionRange = null; if (deltaSet) { redoSelectionRange = session.redoChanges(deltaSet, dontSelect); this.$undoStack.push(deltaSet); this.$syncRev(); } this.$fromUndo = false; return redoSelectionRange; }; this.$syncRev = function() { var stack = this.$undoStack; var nextDelta = stack[stack.length - 1]; var id = nextDelta && nextDelta[0].id || 0; this.$redoStackBaseRev = id; this.$rev = id; }; /** * Destroys the stack of undo and redo redo operations. **/ this.reset = function() { this.lastDeltas = null; this.$lastDelta = null; this.$undoStack = []; this.$redoStack = []; this.$rev = 0; this.mark = 0; this.$redoStackBaseRev = this.$rev; this.selections = []; }; /** * Returns `true` if there are undo operations left to perform. * @returns {Boolean} **/ this.canUndo = function() { return this.$undoStack.length > 0; }; /** * Returns `true` if there are redo operations left to perform. * @returns {Boolean} **/ this.canRedo = function() { return this.$redoStack.length > 0; }; /** * Marks the current status clean **/ this.bookmark = function(rev) { if (rev == undefined) rev = this.$rev; this.mark = rev; }; /** * Returns if the current status is clean * @returns {Boolean} **/ this.isAtBookmark = function() { return this.$rev === this.mark; }; this.toJSON = function() { }; this.fromJSON = function() { }; this.hasUndo = this.canUndo; this.hasRedo = this.canRedo; this.isClean = this.isAtBookmark; this.markClean = this.bookmark; this.$prettyPrint = function(delta) { if (delta) return stringifyDelta(delta); return stringifyDelta(this.$undoStack) + "\n---\n" + stringifyDelta(this.$redoStack); }; }).call(UndoManager.prototype); function rearrangeUndoStack(stack, pos) { for (var i = pos; i--; ) { var deltaSet = stack[i]; if (deltaSet && !deltaSet[0].ignore) { while(i < pos - 1) { var swapped = swapGroups(stack[i], stack[i + 1]); stack[i] = swapped[0]; stack[i + 1] = swapped[1]; i++; } return true; } } } var Range = require("./range").Range; var cmp = Range.comparePoints; var comparePoints = Range.comparePoints; function $updateMarkers(delta) { var isInsert = delta.action == "insert"; var start = delta.start; var end = delta.end; var rowShift = (end.row - start.row) * (isInsert ? 1 : -1); var colShift = (end.column - start.column) * (isInsert ? 1 : -1); if (isInsert) end = start; for (var i in this.marks) { var point = this.marks[i]; var cmp = comparePoints(point, start); if (cmp < 0) { continue; // delta starts after the range } if (cmp === 0) { if (isInsert) { if (point.bias == 1) { cmp = 1; } else { point.bias == -1; continue; } } } var cmp2 = isInsert ? cmp : comparePoints(point, end); if (cmp2 > 0) { point.row += rowShift; point.column += point.row == end.row ? colShift : 0; continue; } if (!isInsert && cmp2 <= 0) { point.row = start.row; point.column = start.column; if (cmp2 === 0) point.bias = 1; } } } function clonePos(pos) { return {row: pos.row,column: pos.column}; } function cloneDelta(d) { return { start: clonePos(d.start), end: clonePos(d.end), action: d.action, lines: d.lines.slice() }; } function stringifyDelta(d) { d = d || this; if (Array.isArray(d)) { return d.map(stringifyDelta).join("\n"); } var type = ""; if (d.action) { type = d.action == "insert" ? "+" : "-"; type += "[" + d.lines + "]"; } else if (d.value) { if (Array.isArray(d.value)) { type = d.value.map(stringifyRange).join("\n"); } else { type = stringifyRange(d.value); } } if (d.start) { type += stringifyRange(d); } if (d.id || d.rev) { type += "\t(" + (d.id || d.rev) + ")"; } return type; } function stringifyRange(r) { return r.start.row + ":" + r.start.column + "=>" + r.end.row + ":" + r.end.column; } /* * i i d1 d2 * |/ |/ d2.s >= d1.e shift(d2, d1, -1) * d2.s <= d1.s shift(d1, d2, +1) * d1.s < d2.s < d1.e // can split * * i r d1 d2 * |/ |\ d2.s >= d1.e shift(d2, d1, -1) * d2.e <= d1.s shift(d1, d2, -1) * else // can't swap * * r i d1 d2 * |\ |/ d2.s >= d1.s shift(d2, d1, +1) * d2.s <= d1.s shift(d1, d2, +1) * // no else * * r r d1 d2 * |\ |\ d2.s >= d1.s shift(d2, d1, +1) * d2.e <= d1.s shift(d1, d2, -1) * d2.s < d1.s < d2.e // can split */ function swap(d1, d2) { var i1 = d1.action == "insert"; var i2 = d2.action == "insert"; if (i1 && i2) { if (cmp(d2.start, d1.end) >= 0) { shift(d2, d1, -1); } else if (cmp(d2.start, d1.start) <= 0) { shift(d1, d2, +1); } else { return null; } } else if (i1 && !i2) { if (cmp(d2.start, d1.end) >= 0) { shift(d2, d1, -1); } else if (cmp(d2.end, d1.start) <= 0) { shift(d1, d2, -1); } else { return null; } } else if (!i1 && i2) { if (cmp(d2.start, d1.start) >= 0) { shift(d2, d1, +1); } else if (cmp(d2.start, d1.start) <= 0) { shift(d1, d2, +1); } else { return null; } } else if (!i1 && !i2) { if (cmp(d2.start, d1.start) >= 0) { shift(d2, d1, +1); } else if (cmp(d2.end, d1.start) <= 0) { shift(d1, d2, -1); } else { return null; } } return [d2, d1]; } function swapGroups(ds1, ds2) { for (var i = ds1.length; i--; ) { for (var j = 0; j < ds2.length; j++) { if (!swap(ds1[i], ds2[j])) { // rollback, we have to undo ds2 first while (i < ds1.length) { while (j--) { swap(ds2[j], ds1[i]); } j = ds2.length; i++; } return [ds1, ds2]; } } } ds1.selectionBefore = ds2.selectionBefore = ds1.selectionAfter = ds2.selectionAfter = null; return [ds2, ds1]; } /* d2 xform(d1, c1) = [d2, c2] o<---o xform(c1, d1) = [c2, d2] c2 | | d1 o<---o c1 */ function xform(d1, c1) { var i1 = d1.action == "insert"; var i2 = c1.action == "insert"; if (i1 && i2) { if (cmp(d1.start, c1.start) < 0) { shift(c1, d1, 1); } else { shift(d1, c1, 1); } } else if (i1 && !i2) { if (cmp(d1.start, c1.end) >= 0) { shift(d1, c1, -1); } else if (cmp(d1.start, c1.start) <= 0) { shift(c1, d1, +1); } else { shift(d1, Range.fromPoints(c1.start, d1.start), -1); shift(c1, d1, +1); } } else if (!i1 && i2) { if (cmp(c1.start, d1.end) >= 0) { shift(c1, d1, -1); } else if (cmp(c1.start, d1.start) <= 0) { shift(d1, c1, +1); } else { shift(c1, Range.fromPoints(d1.start, c1.start), -1); shift(d1, c1, +1); } } else if (!i1 && !i2) { if (cmp(c1.start, d1.end) >= 0) { shift(c1, d1, -1); } else if (cmp(c1.end, d1.start) <= 0) { shift(d1, c1, -1); } else { var before, after; if (cmp(d1.start, c1.start) < 0) { before = d1; d1 = splitDelta(d1, c1.start); } if (cmp(d1.end, c1.end) > 0) { after = splitDelta(d1, c1.end); } shiftPos(c1.end, d1.start, d1.end, -1); if (after && !before) { d1.lines = after.lines; d1.start = after.start; d1.end = after.end; after = d1; } return [c1, before, after].filter(Boolean); } } return [c1, d1]; } function shift(d1, d2, dir) { shiftPos(d1.start, d2.start, d2.end, dir); shiftPos(d1.end, d2.start, d2.end, dir); } function shiftPos(pos, start, end, dir) { if (pos.row == (dir == 1 ? start : end).row) { pos.column += dir * (end.column - start.column); } pos.row += dir * (end.row - start.row); } function splitDelta(c, pos) { var lines = c.lines; var end = c.end; c.end = clonePos(pos); var rowsBefore = c.end.row - c.start.row; var otherLines = lines.splice(rowsBefore, lines.length); var col = rowsBefore ? pos.column : pos.column - c.start.column; lines.push(otherLines[0].substring(0, col)); otherLines[0] = otherLines[0].substr(col) ; var rest = { start: clonePos(pos), end: end, lines: otherLines, action: c.action }; return rest; } function moveDeltasByOne(redoStack, d) { d = cloneDelta(d); for (var j = redoStack.length; j--;) { var deltaSet = redoStack[j]; for (var i = 0; i < deltaSet.length; i++) { var x = deltaSet[i]; var xformed = xform(x, d); d = xformed[0]; if (xformed.length != 2) { if (xformed[2]) { deltaSet.splice(i + 1, 1, xformed[1], xformed[2]); i++; } else if (!xformed[1]) { deltaSet.splice(i, 1); i--; } } } if (!deltaSet.length) { redoStack.splice(j, 1); } } return redoStack; } function rebaseRedoStack(redoStack, deltaSets) { for (var i = 0; i < deltaSets.length; i++) { var deltas = deltaSets[i]; for (var j = 0; j < deltas.length; j++) { moveDeltasByOne(redoStack, deltas[j]); } } } exports.UndoManager = UndoManager; });