openrat-cms

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

changes.js (11984B)


      1 import { retreatFrontier } from "../line/highlight.js"
      2 import { startWorker } from "../display/highlight_worker.js"
      3 import { operation } from "../display/operations.js"
      4 import { regChange, regLineChange } from "../display/view_tracking.js"
      5 import { clipLine, clipPos, cmp, Pos } from "../line/pos.js"
      6 import { sawReadOnlySpans } from "../line/saw_special_spans.js"
      7 import { lineLength, removeReadOnlyRanges, stretchSpansOverChange, visualLine } from "../line/spans.js"
      8 import { getBetween, getLine, lineNo } from "../line/utils_line.js"
      9 import { estimateHeight } from "../measurement/position_measurement.js"
     10 import { hasHandler, signal, signalCursorActivity } from "../util/event.js"
     11 import { indexOf, lst, map, sel_dontScroll } from "../util/misc.js"
     12 import { signalLater } from "../util/operation_group.js"
     13 
     14 import { changeEnd, computeSelAfterChange } from "./change_measurement.js"
     15 import { isWholeLineUpdate, linkedDocs, updateDoc } from "./document_data.js"
     16 import { addChangeToHistory, historyChangeFromChange, mergeOldSpans, pushSelectionToHistory } from "./history.js"
     17 import { Range, Selection } from "./selection.js"
     18 import { setSelection, setSelectionNoUndo } from "./selection_updates.js"
     19 
     20 // UPDATING
     21 
     22 // Allow "beforeChange" event handlers to influence a change
     23 function filterChange(doc, change, update) {
     24   let obj = {
     25     canceled: false,
     26     from: change.from,
     27     to: change.to,
     28     text: change.text,
     29     origin: change.origin,
     30     cancel: () => obj.canceled = true
     31   }
     32   if (update) obj.update = (from, to, text, origin) => {
     33     if (from) obj.from = clipPos(doc, from)
     34     if (to) obj.to = clipPos(doc, to)
     35     if (text) obj.text = text
     36     if (origin !== undefined) obj.origin = origin
     37   }
     38   signal(doc, "beforeChange", doc, obj)
     39   if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj)
     40 
     41   if (obj.canceled) return null
     42   return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}
     43 }
     44 
     45 // Apply a change to a document, and add it to the document's
     46 // history, and propagating it to all linked documents.
     47 export function makeChange(doc, change, ignoreReadOnly) {
     48   if (doc.cm) {
     49     if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly)
     50     if (doc.cm.state.suppressEdits) return
     51   }
     52 
     53   if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
     54     change = filterChange(doc, change, true)
     55     if (!change) return
     56   }
     57 
     58   // Possibly split or suppress the update based on the presence
     59   // of read-only spans in its range.
     60   let split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to)
     61   if (split) {
     62     for (let i = split.length - 1; i >= 0; --i)
     63       makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin})
     64   } else {
     65     makeChangeInner(doc, change)
     66   }
     67 }
     68 
     69 function makeChangeInner(doc, change) {
     70   if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return
     71   let selAfter = computeSelAfterChange(doc, change)
     72   addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN)
     73 
     74   makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change))
     75   let rebased = []
     76 
     77   linkedDocs(doc, (doc, sharedHist) => {
     78     if (!sharedHist && indexOf(rebased, doc.history) == -1) {
     79       rebaseHist(doc.history, change)
     80       rebased.push(doc.history)
     81     }
     82     makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change))
     83   })
     84 }
     85 
     86 // Revert a change stored in a document's history.
     87 export function makeChangeFromHistory(doc, type, allowSelectionOnly) {
     88   if (doc.cm && doc.cm.state.suppressEdits && !allowSelectionOnly) return
     89 
     90   let hist = doc.history, event, selAfter = doc.sel
     91   let source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done
     92 
     93   // Verify that there is a useable event (so that ctrl-z won't
     94   // needlessly clear selection events)
     95   let i = 0
     96   for (; i < source.length; i++) {
     97     event = source[i]
     98     if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges)
     99       break
    100   }
    101   if (i == source.length) return
    102   hist.lastOrigin = hist.lastSelOrigin = null
    103 
    104   for (;;) {
    105     event = source.pop()
    106     if (event.ranges) {
    107       pushSelectionToHistory(event, dest)
    108       if (allowSelectionOnly && !event.equals(doc.sel)) {
    109         setSelection(doc, event, {clearRedo: false})
    110         return
    111       }
    112       selAfter = event
    113     }
    114     else break
    115   }
    116 
    117   // Build up a reverse change object to add to the opposite history
    118   // stack (redo when undoing, and vice versa).
    119   let antiChanges = []
    120   pushSelectionToHistory(selAfter, dest)
    121   dest.push({changes: antiChanges, generation: hist.generation})
    122   hist.generation = event.generation || ++hist.maxGeneration
    123 
    124   let filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")
    125 
    126   for (let i = event.changes.length - 1; i >= 0; --i) {
    127     let change = event.changes[i]
    128     change.origin = type
    129     if (filter && !filterChange(doc, change, false)) {
    130       source.length = 0
    131       return
    132     }
    133 
    134     antiChanges.push(historyChangeFromChange(doc, change))
    135 
    136     let after = i ? computeSelAfterChange(doc, change) : lst(source)
    137     makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change))
    138     if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)})
    139     let rebased = []
    140 
    141     // Propagate to the linked documents
    142     linkedDocs(doc, (doc, sharedHist) => {
    143       if (!sharedHist && indexOf(rebased, doc.history) == -1) {
    144         rebaseHist(doc.history, change)
    145         rebased.push(doc.history)
    146       }
    147       makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change))
    148     })
    149   }
    150 }
    151 
    152 // Sub-views need their line numbers shifted when text is added
    153 // above or below them in the parent document.
    154 function shiftDoc(doc, distance) {
    155   if (distance == 0) return
    156   doc.first += distance
    157   doc.sel = new Selection(map(doc.sel.ranges, range => new Range(
    158     Pos(range.anchor.line + distance, range.anchor.ch),
    159     Pos(range.head.line + distance, range.head.ch)
    160   )), doc.sel.primIndex)
    161   if (doc.cm) {
    162     regChange(doc.cm, doc.first, doc.first - distance, distance)
    163     for (let d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)
    164       regLineChange(doc.cm, l, "gutter")
    165   }
    166 }
    167 
    168 // More lower-level change function, handling only a single document
    169 // (not linked ones).
    170 function makeChangeSingleDoc(doc, change, selAfter, spans) {
    171   if (doc.cm && !doc.cm.curOp)
    172     return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans)
    173 
    174   if (change.to.line < doc.first) {
    175     shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line))
    176     return
    177   }
    178   if (change.from.line > doc.lastLine()) return
    179 
    180   // Clip the change to the size of this doc
    181   if (change.from.line < doc.first) {
    182     let shift = change.text.length - 1 - (doc.first - change.from.line)
    183     shiftDoc(doc, shift)
    184     change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
    185               text: [lst(change.text)], origin: change.origin}
    186   }
    187   let last = doc.lastLine()
    188   if (change.to.line > last) {
    189     change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
    190               text: [change.text[0]], origin: change.origin}
    191   }
    192 
    193   change.removed = getBetween(doc, change.from, change.to)
    194 
    195   if (!selAfter) selAfter = computeSelAfterChange(doc, change)
    196   if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans)
    197   else updateDoc(doc, change, spans)
    198   setSelectionNoUndo(doc, selAfter, sel_dontScroll)
    199 }
    200 
    201 // Handle the interaction of a change to a document with the editor
    202 // that this document is part of.
    203 function makeChangeSingleDocInEditor(cm, change, spans) {
    204   let doc = cm.doc, display = cm.display, from = change.from, to = change.to
    205 
    206   let recomputeMaxLength = false, checkWidthStart = from.line
    207   if (!cm.options.lineWrapping) {
    208     checkWidthStart = lineNo(visualLine(getLine(doc, from.line)))
    209     doc.iter(checkWidthStart, to.line + 1, line => {
    210       if (line == display.maxLine) {
    211         recomputeMaxLength = true
    212         return true
    213       }
    214     })
    215   }
    216 
    217   if (doc.sel.contains(change.from, change.to) > -1)
    218     signalCursorActivity(cm)
    219 
    220   updateDoc(doc, change, spans, estimateHeight(cm))
    221 
    222   if (!cm.options.lineWrapping) {
    223     doc.iter(checkWidthStart, from.line + change.text.length, line => {
    224       let len = lineLength(line)
    225       if (len > display.maxLineLength) {
    226         display.maxLine = line
    227         display.maxLineLength = len
    228         display.maxLineChanged = true
    229         recomputeMaxLength = false
    230       }
    231     })
    232     if (recomputeMaxLength) cm.curOp.updateMaxLine = true
    233   }
    234 
    235   retreatFrontier(doc, from.line)
    236   startWorker(cm, 400)
    237 
    238   let lendiff = change.text.length - (to.line - from.line) - 1
    239   // Remember that these lines changed, for updating the display
    240   if (change.full)
    241     regChange(cm)
    242   else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change))
    243     regLineChange(cm, from.line, "text")
    244   else
    245     regChange(cm, from.line, to.line + 1, lendiff)
    246 
    247   let changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change")
    248   if (changeHandler || changesHandler) {
    249     let obj = {
    250       from: from, to: to,
    251       text: change.text,
    252       removed: change.removed,
    253       origin: change.origin
    254     }
    255     if (changeHandler) signalLater(cm, "change", cm, obj)
    256     if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj)
    257   }
    258   cm.display.selForContextMenu = null
    259 }
    260 
    261 export function replaceRange(doc, code, from, to, origin) {
    262   if (!to) to = from
    263   if (cmp(to, from) < 0) [from, to] = [to, from]
    264   if (typeof code == "string") code = doc.splitLines(code)
    265   makeChange(doc, {from, to, text: code, origin})
    266 }
    267 
    268 // Rebasing/resetting history to deal with externally-sourced changes
    269 
    270 function rebaseHistSelSingle(pos, from, to, diff) {
    271   if (to < pos.line) {
    272     pos.line += diff
    273   } else if (from < pos.line) {
    274     pos.line = from
    275     pos.ch = 0
    276   }
    277 }
    278 
    279 // Tries to rebase an array of history events given a change in the
    280 // document. If the change touches the same lines as the event, the
    281 // event, and everything 'behind' it, is discarded. If the change is
    282 // before the event, the event's positions are updated. Uses a
    283 // copy-on-write scheme for the positions, to avoid having to
    284 // reallocate them all on every rebase, but also avoid problems with
    285 // shared position objects being unsafely updated.
    286 function rebaseHistArray(array, from, to, diff) {
    287   for (let i = 0; i < array.length; ++i) {
    288     let sub = array[i], ok = true
    289     if (sub.ranges) {
    290       if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true }
    291       for (let j = 0; j < sub.ranges.length; j++) {
    292         rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff)
    293         rebaseHistSelSingle(sub.ranges[j].head, from, to, diff)
    294       }
    295       continue
    296     }
    297     for (let j = 0; j < sub.changes.length; ++j) {
    298       let cur = sub.changes[j]
    299       if (to < cur.from.line) {
    300         cur.from = Pos(cur.from.line + diff, cur.from.ch)
    301         cur.to = Pos(cur.to.line + diff, cur.to.ch)
    302       } else if (from <= cur.to.line) {
    303         ok = false
    304         break
    305       }
    306     }
    307     if (!ok) {
    308       array.splice(0, i + 1)
    309       i = 0
    310     }
    311   }
    312 }
    313 
    314 function rebaseHist(hist, change) {
    315   let from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1
    316   rebaseHistArray(hist.done, from, to, diff)
    317   rebaseHistArray(hist.undone, from, to, diff)
    318 }
    319 
    320 // Utility for applying a change to a line by handle or number,
    321 // returning the number and optionally registering the line as
    322 // changed.
    323 export function changeLine(doc, handle, changeType, op) {
    324   let no = handle, line = handle
    325   if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle))
    326   else no = lineNo(handle)
    327   if (no == null) return null
    328   if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType)
    329   return line
    330 }