diff .cms/lib/codemirror/src/model/mark_text.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/mark_text.js	Fri Oct 11 22:40:23 2024 +0000
@@ -0,0 +1,293 @@
+import { eltP } from "../util/dom.js"
+import { eventMixin, hasHandler, on } from "../util/event.js"
+import { endOperation, operation, runInOp, startOperation } from "../display/operations.js"
+import { clipPos, cmp, Pos } from "../line/pos.js"
+import { lineNo, updateLineHeight } from "../line/utils_line.js"
+import { clearLineMeasurementCacheFor, findViewForLine, textHeight } from "../measurement/position_measurement.js"
+import { seeReadOnlySpans, seeCollapsedSpans } from "../line/saw_special_spans.js"
+import { addMarkedSpan, conflictingCollapsedRange, getMarkedSpanFor, lineIsHidden, lineLength, MarkedSpan, removeMarkedSpan, visualLine } from "../line/spans.js"
+import { copyObj, indexOf, lst } from "../util/misc.js"
+import { signalLater } from "../util/operation_group.js"
+import { widgetHeight } from "../measurement/widgets.js"
+import { regChange, regLineChange } from "../display/view_tracking.js"
+
+import { linkedDocs } from "./document_data.js"
+import { addChangeToHistory } from "./history.js"
+import { reCheckSelection } from "./selection_updates.js"
+
+// TEXTMARKERS
+
+// Created with markText and setBookmark methods. A TextMarker is a
+// handle that can be used to clear or find a marked position in the
+// document. Line objects hold arrays (markedSpans) containing
+// {from, to, marker} object pointing to such marker objects, and
+// indicating that such a marker is present on that line. Multiple
+// lines may point to the same marker when it spans across lines.
+// The spans will have null for their from/to properties when the
+// marker continues beyond the start/end of the line. Markers have
+// links back to the lines they currently touch.
+
+// Collapsed markers have unique ids, in order to be able to order
+// them, which is needed for uniquely determining an outer marker
+// when they overlap (they may nest, but not partially overlap).
+let nextMarkerId = 0
+
+export class TextMarker {
+  constructor(doc, type) {
+    this.lines = []
+    this.type = type
+    this.doc = doc
+    this.id = ++nextMarkerId
+  }
+
+  // Clear the marker.
+  clear() {
+    if (this.explicitlyCleared) return
+    let cm = this.doc.cm, withOp = cm && !cm.curOp
+    if (withOp) startOperation(cm)
+    if (hasHandler(this, "clear")) {
+      let found = this.find()
+      if (found) signalLater(this, "clear", found.from, found.to)
+    }
+    let min = null, max = null
+    for (let i = 0; i < this.lines.length; ++i) {
+      let line = this.lines[i]
+      let span = getMarkedSpanFor(line.markedSpans, this)
+      if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text")
+      else if (cm) {
+        if (span.to != null) max = lineNo(line)
+        if (span.from != null) min = lineNo(line)
+      }
+      line.markedSpans = removeMarkedSpan(line.markedSpans, span)
+      if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm)
+        updateLineHeight(line, textHeight(cm.display))
+    }
+    if (cm && this.collapsed && !cm.options.lineWrapping) for (let i = 0; i < this.lines.length; ++i) {
+      let visual = visualLine(this.lines[i]), len = lineLength(visual)
+      if (len > cm.display.maxLineLength) {
+        cm.display.maxLine = visual
+        cm.display.maxLineLength = len
+        cm.display.maxLineChanged = true
+      }
+    }
+
+    if (min != null && cm && this.collapsed) regChange(cm, min, max + 1)
+    this.lines.length = 0
+    this.explicitlyCleared = true
+    if (this.atomic && this.doc.cantEdit) {
+      this.doc.cantEdit = false
+      if (cm) reCheckSelection(cm.doc)
+    }
+    if (cm) signalLater(cm, "markerCleared", cm, this, min, max)
+    if (withOp) endOperation(cm)
+    if (this.parent) this.parent.clear()
+  }
+
+  // Find the position of the marker in the document. Returns a {from,
+  // to} object by default. Side can be passed to get a specific side
+  // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the
+  // Pos objects returned contain a line object, rather than a line
+  // number (used to prevent looking up the same line twice).
+  find(side, lineObj) {
+    if (side == null && this.type == "bookmark") side = 1
+    let from, to
+    for (let i = 0; i < this.lines.length; ++i) {
+      let line = this.lines[i]
+      let span = getMarkedSpanFor(line.markedSpans, this)
+      if (span.from != null) {
+        from = Pos(lineObj ? line : lineNo(line), span.from)
+        if (side == -1) return from
+      }
+      if (span.to != null) {
+        to = Pos(lineObj ? line : lineNo(line), span.to)
+        if (side == 1) return to
+      }
+    }
+    return from && {from: from, to: to}
+  }
+
+  // Signals that the marker's widget changed, and surrounding layout
+  // should be recomputed.
+  changed() {
+    let pos = this.find(-1, true), widget = this, cm = this.doc.cm
+    if (!pos || !cm) return
+    runInOp(cm, () => {
+      let line = pos.line, lineN = lineNo(pos.line)
+      let view = findViewForLine(cm, lineN)
+      if (view) {
+        clearLineMeasurementCacheFor(view)
+        cm.curOp.selectionChanged = cm.curOp.forceUpdate = true
+      }
+      cm.curOp.updateMaxLine = true
+      if (!lineIsHidden(widget.doc, line) && widget.height != null) {
+        let oldHeight = widget.height
+        widget.height = null
+        let dHeight = widgetHeight(widget) - oldHeight
+        if (dHeight)
+          updateLineHeight(line, line.height + dHeight)
+      }
+      signalLater(cm, "markerChanged", cm, this)
+    })
+  }
+
+  attachLine(line) {
+    if (!this.lines.length && this.doc.cm) {
+      let op = this.doc.cm.curOp
+      if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1)
+        (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this)
+    }
+    this.lines.push(line)
+  }
+
+  detachLine(line) {
+    this.lines.splice(indexOf(this.lines, line), 1)
+    if (!this.lines.length && this.doc.cm) {
+      let op = this.doc.cm.curOp
+      ;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this)
+    }
+  }
+}
+eventMixin(TextMarker)
+
+// Create a marker, wire it up to the right lines, and
+export function markText(doc, from, to, options, type) {
+  // Shared markers (across linked documents) are handled separately
+  // (markTextShared will call out to this again, once per
+  // document).
+  if (options && options.shared) return markTextShared(doc, from, to, options, type)
+  // Ensure we are in an operation.
+  if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type)
+
+  let marker = new TextMarker(doc, type), diff = cmp(from, to)
+  if (options) copyObj(options, marker, false)
+  // Don't connect empty markers unless clearWhenEmpty is false
+  if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false)
+    return marker
+  if (marker.replacedWith) {
+    // Showing up as a widget implies collapsed (widget replaces text)
+    marker.collapsed = true
+    marker.widgetNode = eltP("span", [marker.replacedWith], "CodeMirror-widget")
+    if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true")
+    if (options.insertLeft) marker.widgetNode.insertLeft = true
+  }
+  if (marker.collapsed) {
+    if (conflictingCollapsedRange(doc, from.line, from, to, marker) ||
+        from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker))
+      throw new Error("Inserting collapsed marker partially overlapping an existing one")
+    seeCollapsedSpans()
+  }
+
+  if (marker.addToHistory)
+    addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN)
+
+  let curLine = from.line, cm = doc.cm, updateMaxLine
+  doc.iter(curLine, to.line + 1, line => {
+    if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine)
+      updateMaxLine = true
+    if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0)
+    addMarkedSpan(line, new MarkedSpan(marker,
+                                       curLine == from.line ? from.ch : null,
+                                       curLine == to.line ? to.ch : null), doc.cm && doc.cm.curOp)
+    ++curLine
+  })
+  // lineIsHidden depends on the presence of the spans, so needs a second pass
+  if (marker.collapsed) doc.iter(from.line, to.line + 1, line => {
+    if (lineIsHidden(doc, line)) updateLineHeight(line, 0)
+  })
+
+  if (marker.clearOnEnter) on(marker, "beforeCursorEnter", () => marker.clear())
+
+  if (marker.readOnly) {
+    seeReadOnlySpans()
+    if (doc.history.done.length || doc.history.undone.length)
+      doc.clearHistory()
+  }
+  if (marker.collapsed) {
+    marker.id = ++nextMarkerId
+    marker.atomic = true
+  }
+  if (cm) {
+    // Sync editor state
+    if (updateMaxLine) cm.curOp.updateMaxLine = true
+    if (marker.collapsed)
+      regChange(cm, from.line, to.line + 1)
+    else if (marker.className || marker.startStyle || marker.endStyle || marker.css ||
+             marker.attributes || marker.title)
+      for (let i = from.line; i <= to.line; i++) regLineChange(cm, i, "text")
+    if (marker.atomic) reCheckSelection(cm.doc)
+    signalLater(cm, "markerAdded", cm, marker)
+  }
+  return marker
+}
+
+// SHARED TEXTMARKERS
+
+// A shared marker spans multiple linked documents. It is
+// implemented as a meta-marker-object controlling multiple normal
+// markers.
+export class SharedTextMarker {
+  constructor(markers, primary) {
+    this.markers = markers
+    this.primary = primary
+    for (let i = 0; i < markers.length; ++i)
+      markers[i].parent = this
+  }
+
+  clear() {
+    if (this.explicitlyCleared) return
+    this.explicitlyCleared = true
+    for (let i = 0; i < this.markers.length; ++i)
+      this.markers[i].clear()
+    signalLater(this, "clear")
+  }
+
+  find(side, lineObj) {
+    return this.primary.find(side, lineObj)
+  }
+}
+eventMixin(SharedTextMarker)
+
+function markTextShared(doc, from, to, options, type) {
+  options = copyObj(options)
+  options.shared = false
+  let markers = [markText(doc, from, to, options, type)], primary = markers[0]
+  let widget = options.widgetNode
+  linkedDocs(doc, doc => {
+    if (widget) options.widgetNode = widget.cloneNode(true)
+    markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type))
+    for (let i = 0; i < doc.linked.length; ++i)
+      if (doc.linked[i].isParent) return
+    primary = lst(markers)
+  })
+  return new SharedTextMarker(markers, primary)
+}
+
+export function findSharedMarkers(doc) {
+  return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), m => m.parent)
+}
+
+export function copySharedMarkers(doc, markers) {
+  for (let i = 0; i < markers.length; i++) {
+    let marker = markers[i], pos = marker.find()
+    let mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to)
+    if (cmp(mFrom, mTo)) {
+      let subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type)
+      marker.markers.push(subMark)
+      subMark.parent = marker
+    }
+  }
+}
+
+export function detachSharedMarkers(markers) {
+  for (let i = 0; i < markers.length; i++) {
+    let marker = markers[i], linked = [marker.primary.doc]
+    linkedDocs(marker.primary.doc, d => linked.push(d))
+    for (let j = 0; j < marker.markers.length; j++) {
+      let subMarker = marker.markers[j]
+      if (indexOf(linked, subMarker.doc) == -1) {
+        subMarker.parent = null
+        marker.markers.splice(j--, 1)
+      }
+    }
+  }
+}