diff .cms/lib/codemirror/src/model/changes.js @ 0:78edf6b517a0 draft

24.10
author Coffee CMS <info@coffee-cms.ru>
date Fri, 11 Oct 2024 22:40:23 +0000
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.cms/lib/codemirror/src/model/changes.js	Fri Oct 11 22:40:23 2024 +0000
@@ -0,0 +1,339 @@
+import { retreatFrontier } from "../line/highlight.js"
+import { startWorker } from "../display/highlight_worker.js"
+import { operation } from "../display/operations.js"
+import { regChange, regLineChange } from "../display/view_tracking.js"
+import { clipLine, clipPos, cmp, Pos } from "../line/pos.js"
+import { sawReadOnlySpans } from "../line/saw_special_spans.js"
+import { lineLength, removeReadOnlyRanges, stretchSpansOverChange, visualLine } from "../line/spans.js"
+import { getBetween, getLine, lineNo } from "../line/utils_line.js"
+import { estimateHeight } from "../measurement/position_measurement.js"
+import { hasHandler, signal, signalCursorActivity } from "../util/event.js"
+import { indexOf, lst, map, sel_dontScroll } from "../util/misc.js"
+import { signalLater } from "../util/operation_group.js"
+
+import { changeEnd, computeSelAfterChange } from "./change_measurement.js"
+import { isWholeLineUpdate, linkedDocs, updateDoc } from "./document_data.js"
+import { addChangeToHistory, historyChangeFromChange, mergeOldSpans, pushSelectionToHistory } from "./history.js"
+import { Range, Selection } from "./selection.js"
+import { setSelection, setSelectionNoUndo, skipAtomic } from "./selection_updates.js"
+
+// UPDATING
+
+// Allow "beforeChange" event handlers to influence a change
+function filterChange(doc, change, update) {
+  let obj = {
+    canceled: false,
+    from: change.from,
+    to: change.to,
+    text: change.text,
+    origin: change.origin,
+    cancel: () => obj.canceled = true
+  }
+  if (update) obj.update = (from, to, text, origin) => {
+    if (from) obj.from = clipPos(doc, from)
+    if (to) obj.to = clipPos(doc, to)
+    if (text) obj.text = text
+    if (origin !== undefined) obj.origin = origin
+  }
+  signal(doc, "beforeChange", doc, obj)
+  if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj)
+
+  if (obj.canceled) {
+    if (doc.cm) doc.cm.curOp.updateInput = 2
+    return null
+  }
+  return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}
+}
+
+// Apply a change to a document, and add it to the document's
+// history, and propagating it to all linked documents.
+export function makeChange(doc, change, ignoreReadOnly) {
+  if (doc.cm) {
+    if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly)
+    if (doc.cm.state.suppressEdits) return
+  }
+
+  if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
+    change = filterChange(doc, change, true)
+    if (!change) return
+  }
+
+  // Possibly split or suppress the update based on the presence
+  // of read-only spans in its range.
+  let split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to)
+  if (split) {
+    for (let i = split.length - 1; i >= 0; --i)
+      makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin})
+  } else {
+    makeChangeInner(doc, change)
+  }
+}
+
+function makeChangeInner(doc, change) {
+  if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return
+  let selAfter = computeSelAfterChange(doc, change)
+  addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN)
+
+  makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change))
+  let rebased = []
+
+  linkedDocs(doc, (doc, sharedHist) => {
+    if (!sharedHist && indexOf(rebased, doc.history) == -1) {
+      rebaseHist(doc.history, change)
+      rebased.push(doc.history)
+    }
+    makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change))
+  })
+}
+
+// Revert a change stored in a document's history.
+export function makeChangeFromHistory(doc, type, allowSelectionOnly) {
+  let suppress = doc.cm && doc.cm.state.suppressEdits
+  if (suppress && !allowSelectionOnly) return
+
+  let hist = doc.history, event, selAfter = doc.sel
+  let source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done
+
+  // Verify that there is a useable event (so that ctrl-z won't
+  // needlessly clear selection events)
+  let i = 0
+  for (; i < source.length; i++) {
+    event = source[i]
+    if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges)
+      break
+  }
+  if (i == source.length) return
+  hist.lastOrigin = hist.lastSelOrigin = null
+
+  for (;;) {
+    event = source.pop()
+    if (event.ranges) {
+      pushSelectionToHistory(event, dest)
+      if (allowSelectionOnly && !event.equals(doc.sel)) {
+        setSelection(doc, event, {clearRedo: false})
+        return
+      }
+      selAfter = event
+    } else if (suppress) {
+      source.push(event)
+      return
+    } else break
+  }
+
+  // Build up a reverse change object to add to the opposite history
+  // stack (redo when undoing, and vice versa).
+  let antiChanges = []
+  pushSelectionToHistory(selAfter, dest)
+  dest.push({changes: antiChanges, generation: hist.generation})
+  hist.generation = event.generation || ++hist.maxGeneration
+
+  let filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")
+
+  for (let i = event.changes.length - 1; i >= 0; --i) {
+    let change = event.changes[i]
+    change.origin = type
+    if (filter && !filterChange(doc, change, false)) {
+      source.length = 0
+      return
+    }
+
+    antiChanges.push(historyChangeFromChange(doc, change))
+
+    let after = i ? computeSelAfterChange(doc, change) : lst(source)
+    makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change))
+    if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)})
+    let rebased = []
+
+    // Propagate to the linked documents
+    linkedDocs(doc, (doc, sharedHist) => {
+      if (!sharedHist && indexOf(rebased, doc.history) == -1) {
+        rebaseHist(doc.history, change)
+        rebased.push(doc.history)
+      }
+      makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change))
+    })
+  }
+}
+
+// Sub-views need their line numbers shifted when text is added
+// above or below them in the parent document.
+function shiftDoc(doc, distance) {
+  if (distance == 0) return
+  doc.first += distance
+  doc.sel = new Selection(map(doc.sel.ranges, range => new Range(
+    Pos(range.anchor.line + distance, range.anchor.ch),
+    Pos(range.head.line + distance, range.head.ch)
+  )), doc.sel.primIndex)
+  if (doc.cm) {
+    regChange(doc.cm, doc.first, doc.first - distance, distance)
+    for (let d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)
+      regLineChange(doc.cm, l, "gutter")
+  }
+}
+
+// More lower-level change function, handling only a single document
+// (not linked ones).
+function makeChangeSingleDoc(doc, change, selAfter, spans) {
+  if (doc.cm && !doc.cm.curOp)
+    return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans)
+
+  if (change.to.line < doc.first) {
+    shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line))
+    return
+  }
+  if (change.from.line > doc.lastLine()) return
+
+  // Clip the change to the size of this doc
+  if (change.from.line < doc.first) {
+    let shift = change.text.length - 1 - (doc.first - change.from.line)
+    shiftDoc(doc, shift)
+    change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
+              text: [lst(change.text)], origin: change.origin}
+  }
+  let last = doc.lastLine()
+  if (change.to.line > last) {
+    change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
+              text: [change.text[0]], origin: change.origin}
+  }
+
+  change.removed = getBetween(doc, change.from, change.to)
+
+  if (!selAfter) selAfter = computeSelAfterChange(doc, change)
+  if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans)
+  else updateDoc(doc, change, spans)
+  setSelectionNoUndo(doc, selAfter, sel_dontScroll)
+
+  if (doc.cantEdit && skipAtomic(doc, Pos(doc.firstLine(), 0)))
+    doc.cantEdit = false
+}
+
+// Handle the interaction of a change to a document with the editor
+// that this document is part of.
+function makeChangeSingleDocInEditor(cm, change, spans) {
+  let doc = cm.doc, display = cm.display, from = change.from, to = change.to
+
+  let recomputeMaxLength = false, checkWidthStart = from.line
+  if (!cm.options.lineWrapping) {
+    checkWidthStart = lineNo(visualLine(getLine(doc, from.line)))
+    doc.iter(checkWidthStart, to.line + 1, line => {
+      if (line == display.maxLine) {
+        recomputeMaxLength = true
+        return true
+      }
+    })
+  }
+
+  if (doc.sel.contains(change.from, change.to) > -1)
+    signalCursorActivity(cm)
+
+  updateDoc(doc, change, spans, estimateHeight(cm))
+
+  if (!cm.options.lineWrapping) {
+    doc.iter(checkWidthStart, from.line + change.text.length, line => {
+      let len = lineLength(line)
+      if (len > display.maxLineLength) {
+        display.maxLine = line
+        display.maxLineLength = len
+        display.maxLineChanged = true
+        recomputeMaxLength = false
+      }
+    })
+    if (recomputeMaxLength) cm.curOp.updateMaxLine = true
+  }
+
+  retreatFrontier(doc, from.line)
+  startWorker(cm, 400)
+
+  let lendiff = change.text.length - (to.line - from.line) - 1
+  // Remember that these lines changed, for updating the display
+  if (change.full)
+    regChange(cm)
+  else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change))
+    regLineChange(cm, from.line, "text")
+  else
+    regChange(cm, from.line, to.line + 1, lendiff)
+
+  let changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change")
+  if (changeHandler || changesHandler) {
+    let obj = {
+      from: from, to: to,
+      text: change.text,
+      removed: change.removed,
+      origin: change.origin
+    }
+    if (changeHandler) signalLater(cm, "change", cm, obj)
+    if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj)
+  }
+  cm.display.selForContextMenu = null
+}
+
+export function replaceRange(doc, code, from, to, origin) {
+  if (!to) to = from
+  if (cmp(to, from) < 0) [from, to] = [to, from]
+  if (typeof code == "string") code = doc.splitLines(code)
+  makeChange(doc, {from, to, text: code, origin})
+}
+
+// Rebasing/resetting history to deal with externally-sourced changes
+
+function rebaseHistSelSingle(pos, from, to, diff) {
+  if (to < pos.line) {
+    pos.line += diff
+  } else if (from < pos.line) {
+    pos.line = from
+    pos.ch = 0
+  }
+}
+
+// Tries to rebase an array of history events given a change in the
+// document. If the change touches the same lines as the event, the
+// event, and everything 'behind' it, is discarded. If the change is
+// before the event, the event's positions are updated. Uses a
+// copy-on-write scheme for the positions, to avoid having to
+// reallocate them all on every rebase, but also avoid problems with
+// shared position objects being unsafely updated.
+function rebaseHistArray(array, from, to, diff) {
+  for (let i = 0; i < array.length; ++i) {
+    let sub = array[i], ok = true
+    if (sub.ranges) {
+      if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true }
+      for (let j = 0; j < sub.ranges.length; j++) {
+        rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff)
+        rebaseHistSelSingle(sub.ranges[j].head, from, to, diff)
+      }
+      continue
+    }
+    for (let j = 0; j < sub.changes.length; ++j) {
+      let cur = sub.changes[j]
+      if (to < cur.from.line) {
+        cur.from = Pos(cur.from.line + diff, cur.from.ch)
+        cur.to = Pos(cur.to.line + diff, cur.to.ch)
+      } else if (from <= cur.to.line) {
+        ok = false
+        break
+      }
+    }
+    if (!ok) {
+      array.splice(0, i + 1)
+      i = 0
+    }
+  }
+}
+
+function rebaseHist(hist, change) {
+  let from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1
+  rebaseHistArray(hist.done, from, to, diff)
+  rebaseHistArray(hist.undone, from, to, diff)
+}
+
+// Utility for applying a change to a line by handle or number,
+// returning the number and optionally registering the line as
+// changed.
+export function changeLine(doc, handle, changeType, op) {
+  let no = handle, line = handle
+  if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle))
+  else no = lineNo(handle)
+  if (no == null) return null
+  if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType)
+  return line
+}