85cdc8f8f4a7d873c7bd0db4c870335fb023a159b1b4b50b7c95c9e7cce4582b82114af65b39ca0e0e624edbd47cb409c385c7d7515e5e21fc11e97e55a720 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import { cmp, copyPos } from "../line/pos.js"
  2. import { stretchSpansOverChange } from "../line/spans.js"
  3. import { getBetween } from "../line/utils_line.js"
  4. import { signal } from "../util/event.js"
  5. import { indexOf, lst } from "../util/misc.js"
  6. import { changeEnd } from "./change_measurement.js"
  7. import { linkedDocs } from "./document_data.js"
  8. import { Selection } from "./selection.js"
  9. export function History(prev) {
  10. // Arrays of change events and selections. Doing something adds an
  11. // event to done and clears undo. Undoing moves events from done
  12. // to undone, redoing moves them in the other direction.
  13. this.done = []; this.undone = []
  14. this.undoDepth = prev ? prev.undoDepth : Infinity
  15. // Used to track when changes can be merged into a single undo
  16. // event
  17. this.lastModTime = this.lastSelTime = 0
  18. this.lastOp = this.lastSelOp = null
  19. this.lastOrigin = this.lastSelOrigin = null
  20. // Used by the isClean() method
  21. this.generation = this.maxGeneration = prev ? prev.maxGeneration : 1
  22. }
  23. // Create a history change event from an updateDoc-style change
  24. // object.
  25. export function historyChangeFromChange(doc, change) {
  26. let histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}
  27. attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1)
  28. linkedDocs(doc, doc => attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1), true)
  29. return histChange
  30. }
  31. // Pop all selection events off the end of a history array. Stop at
  32. // a change event.
  33. function clearSelectionEvents(array) {
  34. while (array.length) {
  35. let last = lst(array)
  36. if (last.ranges) array.pop()
  37. else break
  38. }
  39. }
  40. // Find the top change event in the history. Pop off selection
  41. // events that are in the way.
  42. function lastChangeEvent(hist, force) {
  43. if (force) {
  44. clearSelectionEvents(hist.done)
  45. return lst(hist.done)
  46. } else if (hist.done.length && !lst(hist.done).ranges) {
  47. return lst(hist.done)
  48. } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) {
  49. hist.done.pop()
  50. return lst(hist.done)
  51. }
  52. }
  53. // Register a change in the history. Merges changes that are within
  54. // a single operation, or are close together with an origin that
  55. // allows merging (starting with "+") into a single event.
  56. export function addChangeToHistory(doc, change, selAfter, opId) {
  57. let hist = doc.history
  58. hist.undone.length = 0
  59. let time = +new Date, cur
  60. let last
  61. if ((hist.lastOp == opId ||
  62. hist.lastOrigin == change.origin && change.origin &&
  63. ((change.origin.charAt(0) == "+" && hist.lastModTime > time - (doc.cm ? doc.cm.options.historyEventDelay : 500)) ||
  64. change.origin.charAt(0) == "*")) &&
  65. (cur = lastChangeEvent(hist, hist.lastOp == opId))) {
  66. // Merge this change into the last event
  67. last = lst(cur.changes)
  68. if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) {
  69. // Optimized case for simple insertion -- don't want to add
  70. // new changesets for every character typed
  71. last.to = changeEnd(change)
  72. } else {
  73. // Add new sub-event
  74. cur.changes.push(historyChangeFromChange(doc, change))
  75. }
  76. } else {
  77. // Can not be merged, start a new event.
  78. let before = lst(hist.done)
  79. if (!before || !before.ranges)
  80. pushSelectionToHistory(doc.sel, hist.done)
  81. cur = {changes: [historyChangeFromChange(doc, change)],
  82. generation: hist.generation}
  83. hist.done.push(cur)
  84. while (hist.done.length > hist.undoDepth) {
  85. hist.done.shift()
  86. if (!hist.done[0].ranges) hist.done.shift()
  87. }
  88. }
  89. hist.done.push(selAfter)
  90. hist.generation = ++hist.maxGeneration
  91. hist.lastModTime = hist.lastSelTime = time
  92. hist.lastOp = hist.lastSelOp = opId
  93. hist.lastOrigin = hist.lastSelOrigin = change.origin
  94. if (!last) signal(doc, "historyAdded")
  95. }
  96. function selectionEventCanBeMerged(doc, origin, prev, sel) {
  97. let ch = origin.charAt(0)
  98. return ch == "*" ||
  99. ch == "+" &&
  100. prev.ranges.length == sel.ranges.length &&
  101. prev.somethingSelected() == sel.somethingSelected() &&
  102. new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500)
  103. }
  104. // Called whenever the selection changes, sets the new selection as
  105. // the pending selection in the history, and pushes the old pending
  106. // selection into the 'done' array when it was significantly
  107. // different (in number of selected ranges, emptiness, or time).
  108. export function addSelectionToHistory(doc, sel, opId, options) {
  109. let hist = doc.history, origin = options && options.origin
  110. // A new event is started when the previous origin does not match
  111. // the current, or the origins don't allow matching. Origins
  112. // starting with * are always merged, those starting with + are
  113. // merged when similar and close together in time.
  114. if (opId == hist.lastSelOp ||
  115. (origin && hist.lastSelOrigin == origin &&
  116. (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin ||
  117. selectionEventCanBeMerged(doc, origin, lst(hist.done), sel))))
  118. hist.done[hist.done.length - 1] = sel
  119. else
  120. pushSelectionToHistory(sel, hist.done)
  121. hist.lastSelTime = +new Date
  122. hist.lastSelOrigin = origin
  123. hist.lastSelOp = opId
  124. if (options && options.clearRedo !== false)
  125. clearSelectionEvents(hist.undone)
  126. }
  127. export function pushSelectionToHistory(sel, dest) {
  128. let top = lst(dest)
  129. if (!(top && top.ranges && top.equals(sel)))
  130. dest.push(sel)
  131. }
  132. // Used to store marked span information in the history.
  133. function attachLocalSpans(doc, change, from, to) {
  134. let existing = change["spans_" + doc.id], n = 0
  135. doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), line => {
  136. if (line.markedSpans)
  137. (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans
  138. ++n
  139. })
  140. }
  141. // When un/re-doing restores text containing marked spans, those
  142. // that have been explicitly cleared should not be restored.
  143. function removeClearedSpans(spans) {
  144. if (!spans) return null
  145. let out
  146. for (let i = 0; i < spans.length; ++i) {
  147. if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i) }
  148. else if (out) out.push(spans[i])
  149. }
  150. return !out ? spans : out.length ? out : null
  151. }
  152. // Retrieve and filter the old marked spans stored in a change event.
  153. function getOldSpans(doc, change) {
  154. let found = change["spans_" + doc.id]
  155. if (!found) return null
  156. let nw = []
  157. for (let i = 0; i < change.text.length; ++i)
  158. nw.push(removeClearedSpans(found[i]))
  159. return nw
  160. }
  161. // Used for un/re-doing changes from the history. Combines the
  162. // result of computing the existing spans with the set of spans that
  163. // existed in the history (so that deleting around a span and then
  164. // undoing brings back the span).
  165. export function mergeOldSpans(doc, change) {
  166. let old = getOldSpans(doc, change)
  167. let stretched = stretchSpansOverChange(doc, change)
  168. if (!old) return stretched
  169. if (!stretched) return old
  170. for (let i = 0; i < old.length; ++i) {
  171. let oldCur = old[i], stretchCur = stretched[i]
  172. if (oldCur && stretchCur) {
  173. spans: for (let j = 0; j < stretchCur.length; ++j) {
  174. let span = stretchCur[j]
  175. for (let k = 0; k < oldCur.length; ++k)
  176. if (oldCur[k].marker == span.marker) continue spans
  177. oldCur.push(span)
  178. }
  179. } else if (stretchCur) {
  180. old[i] = stretchCur
  181. }
  182. }
  183. return old
  184. }
  185. // Used both to provide a JSON-safe object in .getHistory, and, when
  186. // detaching a document, to split the history in two
  187. export function copyHistoryArray(events, newGroup, instantiateSel) {
  188. let copy = []
  189. for (let i = 0; i < events.length; ++i) {
  190. let event = events[i]
  191. if (event.ranges) {
  192. copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event)
  193. continue
  194. }
  195. let changes = event.changes, newChanges = []
  196. copy.push({changes: newChanges})
  197. for (let j = 0; j < changes.length; ++j) {
  198. let change = changes[j], m
  199. newChanges.push({from: change.from, to: change.to, text: change.text})
  200. if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) {
  201. if (indexOf(newGroup, Number(m[1])) > -1) {
  202. lst(newChanges)[prop] = change[prop]
  203. delete change[prop]
  204. }
  205. }
  206. }
  207. }
  208. return copy
  209. }