openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 }