undomanager.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(ace_require, exports, module) {
  31. "use strict";
  32. /**
  33. * This object maintains the undo stack for an [[EditSession `EditSession`]].
  34. * @class UndoManager
  35. **/
  36. /**
  37. * Resets the current undo state and creates a new `UndoManager`.
  38. *
  39. * @constructor
  40. **/
  41. var UndoManager = function() {
  42. this.$maxRev = 0;
  43. this.$fromUndo = false;
  44. this.reset();
  45. };
  46. (function() {
  47. this.addSession = function(session) {
  48. this.$session = session;
  49. };
  50. /**
  51. * Provides a means for implementing your own undo manager. `options` has one property, `args`, an [[Array `Array`]], with two elements:
  52. *
  53. * - `args[0]` is an array of deltas
  54. * - `args[1]` is the document to associate with
  55. *
  56. * @param {Object} options Contains additional properties
  57. *
  58. **/
  59. this.add = function(delta, allowMerge, session) {
  60. if (this.$fromUndo) return;
  61. if (delta == this.$lastDelta) return;
  62. if (!this.$keepRedoStack) this.$redoStack.length = 0;
  63. if (allowMerge === false || !this.lastDeltas) {
  64. this.lastDeltas = [];
  65. this.$undoStack.push(this.lastDeltas);
  66. delta.id = this.$rev = ++this.$maxRev;
  67. }
  68. if (delta.action == "remove" || delta.action == "insert")
  69. this.$lastDelta = delta;
  70. this.lastDeltas.push(delta);
  71. };
  72. this.addSelection = function(selection, rev) {
  73. this.selections.push({
  74. value: selection,
  75. rev: rev || this.$rev
  76. });
  77. };
  78. this.startNewGroup = function() {
  79. this.lastDeltas = null;
  80. return this.$rev;
  81. };
  82. this.markIgnored = function(from, to) {
  83. if (to == null) to = this.$rev + 1;
  84. var stack = this.$undoStack;
  85. for (var i = stack.length; i--;) {
  86. var delta = stack[i][0];
  87. if (delta.id <= from)
  88. break;
  89. if (delta.id < to)
  90. delta.ignore = true;
  91. }
  92. this.lastDeltas = null;
  93. };
  94. this.getSelection = function(rev, after) {
  95. var stack = this.selections;
  96. for (var i = stack.length; i--;) {
  97. var selection = stack[i];
  98. if (selection.rev < rev) {
  99. if (after)
  100. selection = stack[i + 1];
  101. return selection;
  102. }
  103. }
  104. };
  105. this.getRevision = function() {
  106. return this.$rev;
  107. };
  108. this.getDeltas = function(from, to) {
  109. if (to == null) to = this.$rev + 1;
  110. var stack = this.$undoStack;
  111. var end = null, start = 0;
  112. for (var i = stack.length; i--;) {
  113. var delta = stack[i][0];
  114. if (delta.id < to && !end)
  115. end = i+1;
  116. if (delta.id <= from) {
  117. start = i + 1;
  118. break;
  119. }
  120. }
  121. return stack.slice(start, end);
  122. };
  123. this.getChangedRanges = function(from, to) {
  124. if (to == null) to = this.$rev + 1;
  125. };
  126. this.getChangedLines = function(from, to) {
  127. if (to == null) to = this.$rev + 1;
  128. };
  129. /**
  130. * [Perform an undo operation on the document, reverting the last change.]{: #UndoManager.undo}
  131. * @param {Boolean} dontSelect {:dontSelect}
  132. *
  133. * @returns {Range} The range of the undo.
  134. **/
  135. this.undo = function(session, dontSelect) {
  136. this.lastDeltas = null;
  137. var stack = this.$undoStack;
  138. if (!rearrangeUndoStack(stack, stack.length))
  139. return;
  140. if (!session)
  141. session = this.$session;
  142. if (this.$redoStackBaseRev !== this.$rev && this.$redoStack.length)
  143. this.$redoStack = [];
  144. this.$fromUndo = true;
  145. var deltaSet = stack.pop();
  146. var undoSelectionRange = null;
  147. if (deltaSet) {
  148. undoSelectionRange = session.undoChanges(deltaSet, dontSelect);
  149. this.$redoStack.push(deltaSet);
  150. this.$syncRev();
  151. }
  152. this.$fromUndo = false;
  153. return undoSelectionRange;
  154. };
  155. /**
  156. * [Perform a redo operation on the document, reimplementing the last change.]{: #UndoManager.redo}
  157. * @param {Boolean} dontSelect {:dontSelect}
  158. *
  159. **/
  160. this.redo = function(session, dontSelect) {
  161. this.lastDeltas = null;
  162. if (!session)
  163. session = this.$session;
  164. this.$fromUndo = true;
  165. if (this.$redoStackBaseRev != this.$rev) {
  166. var diff = this.getDeltas(this.$redoStackBaseRev, this.$rev + 1);
  167. rebaseRedoStack(this.$redoStack, diff);
  168. this.$redoStackBaseRev = this.$rev;
  169. this.$redoStack.forEach(function(x) {
  170. x[0].id = ++this.$maxRev;
  171. }, this);
  172. }
  173. var deltaSet = this.$redoStack.pop();
  174. var redoSelectionRange = null;
  175. if (deltaSet) {
  176. redoSelectionRange = session.redoChanges(deltaSet, dontSelect);
  177. this.$undoStack.push(deltaSet);
  178. this.$syncRev();
  179. }
  180. this.$fromUndo = false;
  181. return redoSelectionRange;
  182. };
  183. this.$syncRev = function() {
  184. var stack = this.$undoStack;
  185. var nextDelta = stack[stack.length - 1];
  186. var id = nextDelta && nextDelta[0].id || 0;
  187. this.$redoStackBaseRev = id;
  188. this.$rev = id;
  189. };
  190. /**
  191. * Destroys the stack of undo and redo redo operations.
  192. **/
  193. this.reset = function() {
  194. this.lastDeltas = null;
  195. this.$lastDelta = null;
  196. this.$undoStack = [];
  197. this.$redoStack = [];
  198. this.$rev = 0;
  199. this.mark = 0;
  200. this.$redoStackBaseRev = this.$rev;
  201. this.selections = [];
  202. };
  203. /**
  204. * Returns `true` if there are undo operations left to perform.
  205. * @returns {Boolean}
  206. **/
  207. this.canUndo = function() {
  208. return this.$undoStack.length > 0;
  209. };
  210. /**
  211. * Returns `true` if there are redo operations left to perform.
  212. * @returns {Boolean}
  213. **/
  214. this.canRedo = function() {
  215. return this.$redoStack.length > 0;
  216. };
  217. /**
  218. * Marks the current status clean
  219. **/
  220. this.bookmark = function(rev) {
  221. if (rev == undefined)
  222. rev = this.$rev;
  223. this.mark = rev;
  224. };
  225. /**
  226. * Returns if the current status is clean
  227. * @returns {Boolean}
  228. **/
  229. this.isAtBookmark = function() {
  230. return this.$rev === this.mark;
  231. };
  232. this.toJSON = function() {
  233. };
  234. this.fromJSON = function() {
  235. };
  236. this.hasUndo = this.canUndo;
  237. this.hasRedo = this.canRedo;
  238. this.isClean = this.isAtBookmark;
  239. this.markClean = this.bookmark;
  240. this.$prettyPrint = function(delta) {
  241. if (delta) return stringifyDelta(delta);
  242. return stringifyDelta(this.$undoStack) + "\n---\n" + stringifyDelta(this.$redoStack);
  243. };
  244. }).call(UndoManager.prototype);
  245. function rearrangeUndoStack(stack, pos) {
  246. for (var i = pos; i--; ) {
  247. var deltaSet = stack[i];
  248. if (deltaSet && !deltaSet[0].ignore) {
  249. while(i < pos - 1) {
  250. var swapped = swapGroups(stack[i], stack[i + 1]);
  251. stack[i] = swapped[0];
  252. stack[i + 1] = swapped[1];
  253. i++;
  254. }
  255. return true;
  256. }
  257. }
  258. }
  259. var Range = ace_require("./range").Range;
  260. var cmp = Range.comparePoints;
  261. var comparePoints = Range.comparePoints;
  262. function $updateMarkers(delta) {
  263. var isInsert = delta.action == "insert";
  264. var start = delta.start;
  265. var end = delta.end;
  266. var rowShift = (end.row - start.row) * (isInsert ? 1 : -1);
  267. var colShift = (end.column - start.column) * (isInsert ? 1 : -1);
  268. if (isInsert) end = start;
  269. for (var i in this.marks) {
  270. var point = this.marks[i];
  271. var cmp = comparePoints(point, start);
  272. if (cmp < 0) {
  273. continue; // delta starts after the range
  274. }
  275. if (cmp === 0) {
  276. if (isInsert) {
  277. if (point.bias == 1) {
  278. cmp = 1;
  279. }
  280. else {
  281. point.bias == -1;
  282. continue;
  283. }
  284. }
  285. }
  286. var cmp2 = isInsert ? cmp : comparePoints(point, end);
  287. if (cmp2 > 0) {
  288. point.row += rowShift;
  289. point.column += point.row == end.row ? colShift : 0;
  290. continue;
  291. }
  292. if (!isInsert && cmp2 <= 0) {
  293. point.row = start.row;
  294. point.column = start.column;
  295. if (cmp2 === 0)
  296. point.bias = 1;
  297. }
  298. }
  299. }
  300. function clonePos(pos) {
  301. return {row: pos.row,column: pos.column};
  302. }
  303. function cloneDelta(d) {
  304. return {
  305. start: clonePos(d.start),
  306. end: clonePos(d.end),
  307. action: d.action,
  308. lines: d.lines.slice()
  309. };
  310. }
  311. function stringifyDelta(d) {
  312. d = d || this;
  313. if (Array.isArray(d)) {
  314. return d.map(stringifyDelta).join("\n");
  315. }
  316. var type = "";
  317. if (d.action) {
  318. type = d.action == "insert" ? "+" : "-";
  319. type += "[" + d.lines + "]";
  320. } else if (d.value) {
  321. if (Array.isArray(d.value)) {
  322. type = d.value.map(stringifyRange).join("\n");
  323. } else {
  324. type = stringifyRange(d.value);
  325. }
  326. }
  327. if (d.start) {
  328. type += stringifyRange(d);
  329. }
  330. if (d.id || d.rev) {
  331. type += "\t(" + (d.id || d.rev) + ")";
  332. }
  333. return type;
  334. }
  335. function stringifyRange(r) {
  336. return r.start.row + ":" + r.start.column
  337. + "=>" + r.end.row + ":" + r.end.column;
  338. }
  339. /*
  340. * i i d1 d2
  341. * |/ |/ d2.s >= d1.e shift(d2, d1, -1)
  342. * d2.s <= d1.s shift(d1, d2, +1)
  343. * d1.s < d2.s < d1.e // can split
  344. *
  345. * i r d1 d2
  346. * |/ |\ d2.s >= d1.e shift(d2, d1, -1)
  347. * d2.e <= d1.s shift(d1, d2, -1)
  348. * else // can't swap
  349. *
  350. * r i d1 d2
  351. * |\ |/ d2.s >= d1.s shift(d2, d1, +1)
  352. * d2.s <= d1.s shift(d1, d2, +1)
  353. * // no else
  354. *
  355. * r r d1 d2
  356. * |\ |\ d2.s >= d1.s shift(d2, d1, +1)
  357. * d2.e <= d1.s shift(d1, d2, -1)
  358. * d2.s < d1.s < d2.e // can split
  359. */
  360. function swap(d1, d2) {
  361. var i1 = d1.action == "insert";
  362. var i2 = d2.action == "insert";
  363. if (i1 && i2) {
  364. if (cmp(d2.start, d1.end) >= 0) {
  365. shift(d2, d1, -1);
  366. } else if (cmp(d2.start, d1.start) <= 0) {
  367. shift(d1, d2, +1);
  368. } else {
  369. return null;
  370. }
  371. } else if (i1 && !i2) {
  372. if (cmp(d2.start, d1.end) >= 0) {
  373. shift(d2, d1, -1);
  374. } else if (cmp(d2.end, d1.start) <= 0) {
  375. shift(d1, d2, -1);
  376. } else {
  377. return null;
  378. }
  379. } else if (!i1 && i2) {
  380. if (cmp(d2.start, d1.start) >= 0) {
  381. shift(d2, d1, +1);
  382. } else if (cmp(d2.start, d1.start) <= 0) {
  383. shift(d1, d2, +1);
  384. } else {
  385. return null;
  386. }
  387. } else if (!i1 && !i2) {
  388. if (cmp(d2.start, d1.start) >= 0) {
  389. shift(d2, d1, +1);
  390. } else if (cmp(d2.end, d1.start) <= 0) {
  391. shift(d1, d2, -1);
  392. } else {
  393. return null;
  394. }
  395. }
  396. return [d2, d1];
  397. }
  398. function swapGroups(ds1, ds2) {
  399. for (var i = ds1.length; i--; ) {
  400. for (var j = 0; j < ds2.length; j++) {
  401. if (!swap(ds1[i], ds2[j])) {
  402. // rollback, we have to undo ds2 first
  403. while (i < ds1.length) {
  404. while (j--) {
  405. swap(ds2[j], ds1[i]);
  406. }
  407. j = ds2.length;
  408. i++;
  409. }
  410. return [ds1, ds2];
  411. }
  412. }
  413. }
  414. ds1.selectionBefore = ds2.selectionBefore =
  415. ds1.selectionAfter = ds2.selectionAfter = null;
  416. return [ds2, ds1];
  417. }
  418. /*
  419. d2 xform(d1, c1) = [d2, c2]
  420. o<---o xform(c1, d1) = [c2, d2]
  421. c2 | | d1
  422. o<---o
  423. c1
  424. */
  425. function xform(d1, c1) {
  426. var i1 = d1.action == "insert";
  427. var i2 = c1.action == "insert";
  428. if (i1 && i2) {
  429. if (cmp(d1.start, c1.start) < 0) {
  430. shift(c1, d1, 1);
  431. } else {
  432. shift(d1, c1, 1);
  433. }
  434. } else if (i1 && !i2) {
  435. if (cmp(d1.start, c1.end) >= 0) {
  436. shift(d1, c1, -1);
  437. } else if (cmp(d1.start, c1.start) <= 0) {
  438. shift(c1, d1, +1);
  439. } else {
  440. shift(d1, Range.fromPoints(c1.start, d1.start), -1);
  441. shift(c1, d1, +1);
  442. }
  443. } else if (!i1 && i2) {
  444. if (cmp(c1.start, d1.end) >= 0) {
  445. shift(c1, d1, -1);
  446. } else if (cmp(c1.start, d1.start) <= 0) {
  447. shift(d1, c1, +1);
  448. } else {
  449. shift(c1, Range.fromPoints(d1.start, c1.start), -1);
  450. shift(d1, c1, +1);
  451. }
  452. } else if (!i1 && !i2) {
  453. if (cmp(c1.start, d1.end) >= 0) {
  454. shift(c1, d1, -1);
  455. } else if (cmp(c1.end, d1.start) <= 0) {
  456. shift(d1, c1, -1);
  457. } else {
  458. var before, after;
  459. if (cmp(d1.start, c1.start) < 0) {
  460. before = d1;
  461. d1 = splitDelta(d1, c1.start);
  462. }
  463. if (cmp(d1.end, c1.end) > 0) {
  464. after = splitDelta(d1, c1.end);
  465. }
  466. shiftPos(c1.end, d1.start, d1.end, -1);
  467. if (after && !before) {
  468. d1.lines = after.lines;
  469. d1.start = after.start;
  470. d1.end = after.end;
  471. after = d1;
  472. }
  473. return [c1, before, after].filter(Boolean);
  474. }
  475. }
  476. return [c1, d1];
  477. }
  478. function shift(d1, d2, dir) {
  479. shiftPos(d1.start, d2.start, d2.end, dir);
  480. shiftPos(d1.end, d2.start, d2.end, dir);
  481. }
  482. function shiftPos(pos, start, end, dir) {
  483. if (pos.row == (dir == 1 ? start : end).row) {
  484. pos.column += dir * (end.column - start.column);
  485. }
  486. pos.row += dir * (end.row - start.row);
  487. }
  488. function splitDelta(c, pos) {
  489. var lines = c.lines;
  490. var end = c.end;
  491. c.end = clonePos(pos);
  492. var rowsBefore = c.end.row - c.start.row;
  493. var otherLines = lines.splice(rowsBefore, lines.length);
  494. var col = rowsBefore ? pos.column : pos.column - c.start.column;
  495. lines.push(otherLines[0].substring(0, col));
  496. otherLines[0] = otherLines[0].substr(col) ;
  497. var rest = {
  498. start: clonePos(pos),
  499. end: end,
  500. lines: otherLines,
  501. action: c.action
  502. };
  503. return rest;
  504. }
  505. function moveDeltasByOne(redoStack, d) {
  506. d = cloneDelta(d);
  507. for (var j = redoStack.length; j--;) {
  508. var deltaSet = redoStack[j];
  509. for (var i = 0; i < deltaSet.length; i++) {
  510. var x = deltaSet[i];
  511. var xformed = xform(x, d);
  512. d = xformed[0];
  513. if (xformed.length != 2) {
  514. if (xformed[2]) {
  515. deltaSet.splice(i + 1, 1, xformed[1], xformed[2]);
  516. i++;
  517. } else if (!xformed[1]) {
  518. deltaSet.splice(i, 1);
  519. i--;
  520. }
  521. }
  522. }
  523. if (!deltaSet.length) {
  524. redoStack.splice(j, 1);
  525. }
  526. }
  527. return redoStack;
  528. }
  529. function rebaseRedoStack(redoStack, deltaSets) {
  530. for (var i = 0; i < deltaSets.length; i++) {
  531. var deltas = deltaSets[i];
  532. for (var j = 0; j < deltas.length; j++) {
  533. moveDeltasByOne(redoStack, deltas[j]);
  534. }
  535. }
  536. }
  537. exports.UndoManager = UndoManager;
  538. });