123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598 |
- /* ***** 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(ace_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 (!this.$keepRedoStack) this.$redoStack.length = 0;
- 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) {
- 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 = ace_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;
- });
|