index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. import RopeSequence from 'rope-sequence';
  2. import { Mapping } from 'prosemirror-transform';
  3. import { PluginKey, Plugin } from 'prosemirror-state';
  4. // ProseMirror's history isn't simply a way to roll back to a previous
  5. // state, because ProseMirror supports applying changes without adding
  6. // them to the history (for example during collaboration).
  7. //
  8. // To this end, each 'Branch' (one for the undo history and one for
  9. // the redo history) keeps an array of 'Items', which can optionally
  10. // hold a step (an actual undoable change), and always hold a position
  11. // map (which is needed to move changes below them to apply to the
  12. // current document).
  13. //
  14. // An item that has both a step and a selection bookmark is the start
  15. // of an 'event' — a group of changes that will be undone or redone at
  16. // once. (It stores only the bookmark, since that way we don't have to
  17. // provide a document until the selection is actually applied, which
  18. // is useful when compressing.)
  19. // Used to schedule history compression
  20. const max_empty_items = 500;
  21. class Branch {
  22. constructor(items, eventCount) {
  23. this.items = items;
  24. this.eventCount = eventCount;
  25. }
  26. // Pop the latest event off the branch's history and apply it
  27. // to a document transform.
  28. popEvent(state, preserveItems) {
  29. if (this.eventCount == 0)
  30. return null;
  31. let end = this.items.length;
  32. for (;; end--) {
  33. let next = this.items.get(end - 1);
  34. if (next.selection) {
  35. --end;
  36. break;
  37. }
  38. }
  39. let remap, mapFrom;
  40. if (preserveItems) {
  41. remap = this.remapping(end, this.items.length);
  42. mapFrom = remap.maps.length;
  43. }
  44. let transform = state.tr;
  45. let selection, remaining;
  46. let addAfter = [], addBefore = [];
  47. this.items.forEach((item, i) => {
  48. if (!item.step) {
  49. if (!remap) {
  50. remap = this.remapping(end, i + 1);
  51. mapFrom = remap.maps.length;
  52. }
  53. mapFrom--;
  54. addBefore.push(item);
  55. return;
  56. }
  57. if (remap) {
  58. addBefore.push(new Item(item.map));
  59. let step = item.step.map(remap.slice(mapFrom)), map;
  60. if (step && transform.maybeStep(step).doc) {
  61. map = transform.mapping.maps[transform.mapping.maps.length - 1];
  62. addAfter.push(new Item(map, undefined, undefined, addAfter.length + addBefore.length));
  63. }
  64. mapFrom--;
  65. if (map)
  66. remap.appendMap(map, mapFrom);
  67. }
  68. else {
  69. transform.maybeStep(item.step);
  70. }
  71. if (item.selection) {
  72. selection = remap ? item.selection.map(remap.slice(mapFrom)) : item.selection;
  73. remaining = new Branch(this.items.slice(0, end).append(addBefore.reverse().concat(addAfter)), this.eventCount - 1);
  74. return false;
  75. }
  76. }, this.items.length, 0);
  77. return { remaining: remaining, transform, selection: selection };
  78. }
  79. // Create a new branch with the given transform added.
  80. addTransform(transform, selection, histOptions, preserveItems) {
  81. let newItems = [], eventCount = this.eventCount;
  82. let oldItems = this.items, lastItem = !preserveItems && oldItems.length ? oldItems.get(oldItems.length - 1) : null;
  83. for (let i = 0; i < transform.steps.length; i++) {
  84. let step = transform.steps[i].invert(transform.docs[i]);
  85. let item = new Item(transform.mapping.maps[i], step, selection), merged;
  86. if (merged = lastItem && lastItem.merge(item)) {
  87. item = merged;
  88. if (i)
  89. newItems.pop();
  90. else
  91. oldItems = oldItems.slice(0, oldItems.length - 1);
  92. }
  93. newItems.push(item);
  94. if (selection) {
  95. eventCount++;
  96. selection = undefined;
  97. }
  98. if (!preserveItems)
  99. lastItem = item;
  100. }
  101. let overflow = eventCount - histOptions.depth;
  102. if (overflow > DEPTH_OVERFLOW) {
  103. oldItems = cutOffEvents(oldItems, overflow);
  104. eventCount -= overflow;
  105. }
  106. return new Branch(oldItems.append(newItems), eventCount);
  107. }
  108. remapping(from, to) {
  109. let maps = new Mapping;
  110. this.items.forEach((item, i) => {
  111. let mirrorPos = item.mirrorOffset != null && i - item.mirrorOffset >= from
  112. ? maps.maps.length - item.mirrorOffset : undefined;
  113. maps.appendMap(item.map, mirrorPos);
  114. }, from, to);
  115. return maps;
  116. }
  117. addMaps(array) {
  118. if (this.eventCount == 0)
  119. return this;
  120. return new Branch(this.items.append(array.map(map => new Item(map))), this.eventCount);
  121. }
  122. // When the collab module receives remote changes, the history has
  123. // to know about those, so that it can adjust the steps that were
  124. // rebased on top of the remote changes, and include the position
  125. // maps for the remote changes in its array of items.
  126. rebased(rebasedTransform, rebasedCount) {
  127. if (!this.eventCount)
  128. return this;
  129. let rebasedItems = [], start = Math.max(0, this.items.length - rebasedCount);
  130. let mapping = rebasedTransform.mapping;
  131. let newUntil = rebasedTransform.steps.length;
  132. let eventCount = this.eventCount;
  133. this.items.forEach(item => { if (item.selection)
  134. eventCount--; }, start);
  135. let iRebased = rebasedCount;
  136. this.items.forEach(item => {
  137. let pos = mapping.getMirror(--iRebased);
  138. if (pos == null)
  139. return;
  140. newUntil = Math.min(newUntil, pos);
  141. let map = mapping.maps[pos];
  142. if (item.step) {
  143. let step = rebasedTransform.steps[pos].invert(rebasedTransform.docs[pos]);
  144. let selection = item.selection && item.selection.map(mapping.slice(iRebased + 1, pos));
  145. if (selection)
  146. eventCount++;
  147. rebasedItems.push(new Item(map, step, selection));
  148. }
  149. else {
  150. rebasedItems.push(new Item(map));
  151. }
  152. }, start);
  153. let newMaps = [];
  154. for (let i = rebasedCount; i < newUntil; i++)
  155. newMaps.push(new Item(mapping.maps[i]));
  156. let items = this.items.slice(0, start).append(newMaps).append(rebasedItems);
  157. let branch = new Branch(items, eventCount);
  158. if (branch.emptyItemCount() > max_empty_items)
  159. branch = branch.compress(this.items.length - rebasedItems.length);
  160. return branch;
  161. }
  162. emptyItemCount() {
  163. let count = 0;
  164. this.items.forEach(item => { if (!item.step)
  165. count++; });
  166. return count;
  167. }
  168. // Compressing a branch means rewriting it to push the air (map-only
  169. // items) out. During collaboration, these naturally accumulate
  170. // because each remote change adds one. The `upto` argument is used
  171. // to ensure that only the items below a given level are compressed,
  172. // because `rebased` relies on a clean, untouched set of items in
  173. // order to associate old items with rebased steps.
  174. compress(upto = this.items.length) {
  175. let remap = this.remapping(0, upto), mapFrom = remap.maps.length;
  176. let items = [], events = 0;
  177. this.items.forEach((item, i) => {
  178. if (i >= upto) {
  179. items.push(item);
  180. if (item.selection)
  181. events++;
  182. }
  183. else if (item.step) {
  184. let step = item.step.map(remap.slice(mapFrom)), map = step && step.getMap();
  185. mapFrom--;
  186. if (map)
  187. remap.appendMap(map, mapFrom);
  188. if (step) {
  189. let selection = item.selection && item.selection.map(remap.slice(mapFrom));
  190. if (selection)
  191. events++;
  192. let newItem = new Item(map.invert(), step, selection), merged, last = items.length - 1;
  193. if (merged = items.length && items[last].merge(newItem))
  194. items[last] = merged;
  195. else
  196. items.push(newItem);
  197. }
  198. }
  199. else if (item.map) {
  200. mapFrom--;
  201. }
  202. }, this.items.length, 0);
  203. return new Branch(RopeSequence.from(items.reverse()), events);
  204. }
  205. }
  206. Branch.empty = new Branch(RopeSequence.empty, 0);
  207. function cutOffEvents(items, n) {
  208. let cutPoint;
  209. items.forEach((item, i) => {
  210. if (item.selection && (n-- == 0)) {
  211. cutPoint = i;
  212. return false;
  213. }
  214. });
  215. return items.slice(cutPoint);
  216. }
  217. class Item {
  218. constructor(
  219. // The (forward) step map for this item.
  220. map,
  221. // The inverted step
  222. step,
  223. // If this is non-null, this item is the start of a group, and
  224. // this selection is the starting selection for the group (the one
  225. // that was active before the first step was applied)
  226. selection,
  227. // If this item is the inverse of a previous mapping on the stack,
  228. // this points at the inverse's offset
  229. mirrorOffset) {
  230. this.map = map;
  231. this.step = step;
  232. this.selection = selection;
  233. this.mirrorOffset = mirrorOffset;
  234. }
  235. merge(other) {
  236. if (this.step && other.step && !other.selection) {
  237. let step = other.step.merge(this.step);
  238. if (step)
  239. return new Item(step.getMap().invert(), step, this.selection);
  240. }
  241. }
  242. }
  243. // The value of the state field that tracks undo/redo history for that
  244. // state. Will be stored in the plugin state when the history plugin
  245. // is active.
  246. class HistoryState {
  247. constructor(done, undone, prevRanges, prevTime, prevComposition) {
  248. this.done = done;
  249. this.undone = undone;
  250. this.prevRanges = prevRanges;
  251. this.prevTime = prevTime;
  252. this.prevComposition = prevComposition;
  253. }
  254. }
  255. const DEPTH_OVERFLOW = 20;
  256. // Record a transformation in undo history.
  257. function applyTransaction(history, state, tr, options) {
  258. let historyTr = tr.getMeta(historyKey), rebased;
  259. if (historyTr)
  260. return historyTr.historyState;
  261. if (tr.getMeta(closeHistoryKey))
  262. history = new HistoryState(history.done, history.undone, null, 0, -1);
  263. let appended = tr.getMeta("appendedTransaction");
  264. if (tr.steps.length == 0) {
  265. return history;
  266. }
  267. else if (appended && appended.getMeta(historyKey)) {
  268. if (appended.getMeta(historyKey).redo)
  269. return new HistoryState(history.done.addTransform(tr, undefined, options, mustPreserveItems(state)), history.undone, rangesFor(tr.mapping.maps[tr.steps.length - 1]), history.prevTime, history.prevComposition);
  270. else
  271. return new HistoryState(history.done, history.undone.addTransform(tr, undefined, options, mustPreserveItems(state)), null, history.prevTime, history.prevComposition);
  272. }
  273. else if (tr.getMeta("addToHistory") !== false && !(appended && appended.getMeta("addToHistory") === false)) {
  274. // Group transforms that occur in quick succession into one event.
  275. let composition = tr.getMeta("composition");
  276. let newGroup = history.prevTime == 0 ||
  277. (!appended && history.prevComposition != composition &&
  278. (history.prevTime < (tr.time || 0) - options.newGroupDelay || !isAdjacentTo(tr, history.prevRanges)));
  279. let prevRanges = appended ? mapRanges(history.prevRanges, tr.mapping) : rangesFor(tr.mapping.maps[tr.steps.length - 1]);
  280. return new HistoryState(history.done.addTransform(tr, newGroup ? state.selection.getBookmark() : undefined, options, mustPreserveItems(state)), Branch.empty, prevRanges, tr.time, composition == null ? history.prevComposition : composition);
  281. }
  282. else if (rebased = tr.getMeta("rebased")) {
  283. // Used by the collab module to tell the history that some of its
  284. // content has been rebased.
  285. return new HistoryState(history.done.rebased(tr, rebased), history.undone.rebased(tr, rebased), mapRanges(history.prevRanges, tr.mapping), history.prevTime, history.prevComposition);
  286. }
  287. else {
  288. return new HistoryState(history.done.addMaps(tr.mapping.maps), history.undone.addMaps(tr.mapping.maps), mapRanges(history.prevRanges, tr.mapping), history.prevTime, history.prevComposition);
  289. }
  290. }
  291. function isAdjacentTo(transform, prevRanges) {
  292. if (!prevRanges)
  293. return false;
  294. if (!transform.docChanged)
  295. return true;
  296. let adjacent = false;
  297. transform.mapping.maps[0].forEach((start, end) => {
  298. for (let i = 0; i < prevRanges.length; i += 2)
  299. if (start <= prevRanges[i + 1] && end >= prevRanges[i])
  300. adjacent = true;
  301. });
  302. return adjacent;
  303. }
  304. function rangesFor(map) {
  305. let result = [];
  306. map.forEach((_from, _to, from, to) => result.push(from, to));
  307. return result;
  308. }
  309. function mapRanges(ranges, mapping) {
  310. if (!ranges)
  311. return null;
  312. let result = [];
  313. for (let i = 0; i < ranges.length; i += 2) {
  314. let from = mapping.map(ranges[i], 1), to = mapping.map(ranges[i + 1], -1);
  315. if (from <= to)
  316. result.push(from, to);
  317. }
  318. return result;
  319. }
  320. // Apply the latest event from one branch to the document and shift the event
  321. // onto the other branch.
  322. function histTransaction(history, state, dispatch, redo) {
  323. let preserveItems = mustPreserveItems(state);
  324. let histOptions = historyKey.get(state).spec.config;
  325. let pop = (redo ? history.undone : history.done).popEvent(state, preserveItems);
  326. if (!pop)
  327. return;
  328. let selection = pop.selection.resolve(pop.transform.doc);
  329. let added = (redo ? history.done : history.undone).addTransform(pop.transform, state.selection.getBookmark(), histOptions, preserveItems);
  330. let newHist = new HistoryState(redo ? added : pop.remaining, redo ? pop.remaining : added, null, 0, -1);
  331. dispatch(pop.transform.setSelection(selection).setMeta(historyKey, { redo, historyState: newHist }).scrollIntoView());
  332. }
  333. let cachedPreserveItems = false, cachedPreserveItemsPlugins = null;
  334. // Check whether any plugin in the given state has a
  335. // `historyPreserveItems` property in its spec, in which case we must
  336. // preserve steps exactly as they came in, so that they can be
  337. // rebased.
  338. function mustPreserveItems(state) {
  339. let plugins = state.plugins;
  340. if (cachedPreserveItemsPlugins != plugins) {
  341. cachedPreserveItems = false;
  342. cachedPreserveItemsPlugins = plugins;
  343. for (let i = 0; i < plugins.length; i++)
  344. if (plugins[i].spec.historyPreserveItems) {
  345. cachedPreserveItems = true;
  346. break;
  347. }
  348. }
  349. return cachedPreserveItems;
  350. }
  351. /**
  352. Set a flag on the given transaction that will prevent further steps
  353. from being appended to an existing history event (so that they
  354. require a separate undo command to undo).
  355. */
  356. function closeHistory(tr) {
  357. return tr.setMeta(closeHistoryKey, true);
  358. }
  359. const historyKey = new PluginKey("history");
  360. const closeHistoryKey = new PluginKey("closeHistory");
  361. /**
  362. Returns a plugin that enables the undo history for an editor. The
  363. plugin will track undo and redo stacks, which can be used with the
  364. [`undo`](https://prosemirror.net/docs/ref/#history.undo) and [`redo`](https://prosemirror.net/docs/ref/#history.redo) commands.
  365. You can set an `"addToHistory"` [metadata
  366. property](https://prosemirror.net/docs/ref/#state.Transaction.setMeta) of `false` on a transaction
  367. to prevent it from being rolled back by undo.
  368. */
  369. function history(config = {}) {
  370. config = { depth: config.depth || 100,
  371. newGroupDelay: config.newGroupDelay || 500 };
  372. return new Plugin({
  373. key: historyKey,
  374. state: {
  375. init() {
  376. return new HistoryState(Branch.empty, Branch.empty, null, 0, -1);
  377. },
  378. apply(tr, hist, state) {
  379. return applyTransaction(hist, state, tr, config);
  380. }
  381. },
  382. config,
  383. props: {
  384. handleDOMEvents: {
  385. beforeinput(view, e) {
  386. let inputType = e.inputType;
  387. let command = inputType == "historyUndo" ? undo : inputType == "historyRedo" ? redo : null;
  388. if (!command)
  389. return false;
  390. e.preventDefault();
  391. return command(view.state, view.dispatch);
  392. }
  393. }
  394. }
  395. });
  396. }
  397. /**
  398. A command function that undoes the last change, if any.
  399. */
  400. const undo = (state, dispatch) => {
  401. let hist = historyKey.getState(state);
  402. if (!hist || hist.done.eventCount == 0)
  403. return false;
  404. if (dispatch)
  405. histTransaction(hist, state, dispatch, false);
  406. return true;
  407. };
  408. /**
  409. A command function that redoes the last undone change, if any.
  410. */
  411. const redo = (state, dispatch) => {
  412. let hist = historyKey.getState(state);
  413. if (!hist || hist.undone.eventCount == 0)
  414. return false;
  415. if (dispatch)
  416. histTransaction(hist, state, dispatch, true);
  417. return true;
  418. };
  419. /**
  420. The amount of undoable events available in a given state.
  421. */
  422. function undoDepth(state) {
  423. let hist = historyKey.getState(state);
  424. return hist ? hist.done.eventCount : 0;
  425. }
  426. /**
  427. The amount of redoable events available in a given editor state.
  428. */
  429. function redoDepth(state) {
  430. let hist = historyKey.getState(state);
  431. return hist ? hist.undone.eventCount : 0;
  432. }
  433. export { closeHistory, history, redo, redoDepth, undo, undoDepth };