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