openrat-cms

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

mark_text.js (11166B)


      1 import { eltP } from "../util/dom.js"
      2 import { eventMixin, hasHandler, on } from "../util/event.js"
      3 import { endOperation, operation, runInOp, startOperation } from "../display/operations.js"
      4 import { clipPos, cmp, Pos } from "../line/pos.js"
      5 import { lineNo, updateLineHeight } from "../line/utils_line.js"
      6 import { clearLineMeasurementCacheFor, findViewForLine, textHeight } from "../measurement/position_measurement.js"
      7 import { seeReadOnlySpans, seeCollapsedSpans } from "../line/saw_special_spans.js"
      8 import { addMarkedSpan, conflictingCollapsedRange, getMarkedSpanFor, lineIsHidden, lineLength, MarkedSpan, removeMarkedSpan, visualLine } from "../line/spans.js"
      9 import { copyObj, indexOf, lst } from "../util/misc.js"
     10 import { signalLater } from "../util/operation_group.js"
     11 import { widgetHeight } from "../measurement/widgets.js"
     12 import { regChange, regLineChange } from "../display/view_tracking.js"
     13 
     14 import { linkedDocs } from "./document_data.js"
     15 import { addChangeToHistory } from "./history.js"
     16 import { reCheckSelection } from "./selection_updates.js"
     17 
     18 // TEXTMARKERS
     19 
     20 // Created with markText and setBookmark methods. A TextMarker is a
     21 // handle that can be used to clear or find a marked position in the
     22 // document. Line objects hold arrays (markedSpans) containing
     23 // {from, to, marker} object pointing to such marker objects, and
     24 // indicating that such a marker is present on that line. Multiple
     25 // lines may point to the same marker when it spans across lines.
     26 // The spans will have null for their from/to properties when the
     27 // marker continues beyond the start/end of the line. Markers have
     28 // links back to the lines they currently touch.
     29 
     30 // Collapsed markers have unique ids, in order to be able to order
     31 // them, which is needed for uniquely determining an outer marker
     32 // when they overlap (they may nest, but not partially overlap).
     33 let nextMarkerId = 0
     34 
     35 export class TextMarker {
     36   constructor(doc, type) {
     37     this.lines = []
     38     this.type = type
     39     this.doc = doc
     40     this.id = ++nextMarkerId
     41   }
     42 
     43   // Clear the marker.
     44   clear() {
     45     if (this.explicitlyCleared) return
     46     let cm = this.doc.cm, withOp = cm && !cm.curOp
     47     if (withOp) startOperation(cm)
     48     if (hasHandler(this, "clear")) {
     49       let found = this.find()
     50       if (found) signalLater(this, "clear", found.from, found.to)
     51     }
     52     let min = null, max = null
     53     for (let i = 0; i < this.lines.length; ++i) {
     54       let line = this.lines[i]
     55       let span = getMarkedSpanFor(line.markedSpans, this)
     56       if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text")
     57       else if (cm) {
     58         if (span.to != null) max = lineNo(line)
     59         if (span.from != null) min = lineNo(line)
     60       }
     61       line.markedSpans = removeMarkedSpan(line.markedSpans, span)
     62       if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm)
     63         updateLineHeight(line, textHeight(cm.display))
     64     }
     65     if (cm && this.collapsed && !cm.options.lineWrapping) for (let i = 0; i < this.lines.length; ++i) {
     66       let visual = visualLine(this.lines[i]), len = lineLength(visual)
     67       if (len > cm.display.maxLineLength) {
     68         cm.display.maxLine = visual
     69         cm.display.maxLineLength = len
     70         cm.display.maxLineChanged = true
     71       }
     72     }
     73 
     74     if (min != null && cm && this.collapsed) regChange(cm, min, max + 1)
     75     this.lines.length = 0
     76     this.explicitlyCleared = true
     77     if (this.atomic && this.doc.cantEdit) {
     78       this.doc.cantEdit = false
     79       if (cm) reCheckSelection(cm.doc)
     80     }
     81     if (cm) signalLater(cm, "markerCleared", cm, this, min, max)
     82     if (withOp) endOperation(cm)
     83     if (this.parent) this.parent.clear()
     84   }
     85 
     86   // Find the position of the marker in the document. Returns a {from,
     87   // to} object by default. Side can be passed to get a specific side
     88   // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the
     89   // Pos objects returned contain a line object, rather than a line
     90   // number (used to prevent looking up the same line twice).
     91   find(side, lineObj) {
     92     if (side == null && this.type == "bookmark") side = 1
     93     let from, to
     94     for (let i = 0; i < this.lines.length; ++i) {
     95       let line = this.lines[i]
     96       let span = getMarkedSpanFor(line.markedSpans, this)
     97       if (span.from != null) {
     98         from = Pos(lineObj ? line : lineNo(line), span.from)
     99         if (side == -1) return from
    100       }
    101       if (span.to != null) {
    102         to = Pos(lineObj ? line : lineNo(line), span.to)
    103         if (side == 1) return to
    104       }
    105     }
    106     return from && {from: from, to: to}
    107   }
    108 
    109   // Signals that the marker's widget changed, and surrounding layout
    110   // should be recomputed.
    111   changed() {
    112     let pos = this.find(-1, true), widget = this, cm = this.doc.cm
    113     if (!pos || !cm) return
    114     runInOp(cm, () => {
    115       let line = pos.line, lineN = lineNo(pos.line)
    116       let view = findViewForLine(cm, lineN)
    117       if (view) {
    118         clearLineMeasurementCacheFor(view)
    119         cm.curOp.selectionChanged = cm.curOp.forceUpdate = true
    120       }
    121       cm.curOp.updateMaxLine = true
    122       if (!lineIsHidden(widget.doc, line) && widget.height != null) {
    123         let oldHeight = widget.height
    124         widget.height = null
    125         let dHeight = widgetHeight(widget) - oldHeight
    126         if (dHeight)
    127           updateLineHeight(line, line.height + dHeight)
    128       }
    129       signalLater(cm, "markerChanged", cm, this)
    130     })
    131   }
    132 
    133   attachLine(line) {
    134     if (!this.lines.length && this.doc.cm) {
    135       let op = this.doc.cm.curOp
    136       if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1)
    137         (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this)
    138     }
    139     this.lines.push(line)
    140   }
    141 
    142   detachLine(line) {
    143     this.lines.splice(indexOf(this.lines, line), 1)
    144     if (!this.lines.length && this.doc.cm) {
    145       let op = this.doc.cm.curOp
    146       ;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this)
    147     }
    148   }
    149 }
    150 eventMixin(TextMarker)
    151 
    152 // Create a marker, wire it up to the right lines, and
    153 export function markText(doc, from, to, options, type) {
    154   // Shared markers (across linked documents) are handled separately
    155   // (markTextShared will call out to this again, once per
    156   // document).
    157   if (options && options.shared) return markTextShared(doc, from, to, options, type)
    158   // Ensure we are in an operation.
    159   if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type)
    160 
    161   let marker = new TextMarker(doc, type), diff = cmp(from, to)
    162   if (options) copyObj(options, marker, false)
    163   // Don't connect empty markers unless clearWhenEmpty is false
    164   if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false)
    165     return marker
    166   if (marker.replacedWith) {
    167     // Showing up as a widget implies collapsed (widget replaces text)
    168     marker.collapsed = true
    169     marker.widgetNode = eltP("span", [marker.replacedWith], "CodeMirror-widget")
    170     if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true")
    171     if (options.insertLeft) marker.widgetNode.insertLeft = true
    172   }
    173   if (marker.collapsed) {
    174     if (conflictingCollapsedRange(doc, from.line, from, to, marker) ||
    175         from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker))
    176       throw new Error("Inserting collapsed marker partially overlapping an existing one")
    177     seeCollapsedSpans()
    178   }
    179 
    180   if (marker.addToHistory)
    181     addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN)
    182 
    183   let curLine = from.line, cm = doc.cm, updateMaxLine
    184   doc.iter(curLine, to.line + 1, line => {
    185     if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine)
    186       updateMaxLine = true
    187     if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0)
    188     addMarkedSpan(line, new MarkedSpan(marker,
    189                                        curLine == from.line ? from.ch : null,
    190                                        curLine == to.line ? to.ch : null))
    191     ++curLine
    192   })
    193   // lineIsHidden depends on the presence of the spans, so needs a second pass
    194   if (marker.collapsed) doc.iter(from.line, to.line + 1, line => {
    195     if (lineIsHidden(doc, line)) updateLineHeight(line, 0)
    196   })
    197 
    198   if (marker.clearOnEnter) on(marker, "beforeCursorEnter", () => marker.clear())
    199 
    200   if (marker.readOnly) {
    201     seeReadOnlySpans()
    202     if (doc.history.done.length || doc.history.undone.length)
    203       doc.clearHistory()
    204   }
    205   if (marker.collapsed) {
    206     marker.id = ++nextMarkerId
    207     marker.atomic = true
    208   }
    209   if (cm) {
    210     // Sync editor state
    211     if (updateMaxLine) cm.curOp.updateMaxLine = true
    212     if (marker.collapsed)
    213       regChange(cm, from.line, to.line + 1)
    214     else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css)
    215       for (let i = from.line; i <= to.line; i++) regLineChange(cm, i, "text")
    216     if (marker.atomic) reCheckSelection(cm.doc)
    217     signalLater(cm, "markerAdded", cm, marker)
    218   }
    219   return marker
    220 }
    221 
    222 // SHARED TEXTMARKERS
    223 
    224 // A shared marker spans multiple linked documents. It is
    225 // implemented as a meta-marker-object controlling multiple normal
    226 // markers.
    227 export class SharedTextMarker {
    228   constructor(markers, primary) {
    229     this.markers = markers
    230     this.primary = primary
    231     for (let i = 0; i < markers.length; ++i)
    232       markers[i].parent = this
    233   }
    234 
    235   clear() {
    236     if (this.explicitlyCleared) return
    237     this.explicitlyCleared = true
    238     for (let i = 0; i < this.markers.length; ++i)
    239       this.markers[i].clear()
    240     signalLater(this, "clear")
    241   }
    242 
    243   find(side, lineObj) {
    244     return this.primary.find(side, lineObj)
    245   }
    246 }
    247 eventMixin(SharedTextMarker)
    248 
    249 function markTextShared(doc, from, to, options, type) {
    250   options = copyObj(options)
    251   options.shared = false
    252   let markers = [markText(doc, from, to, options, type)], primary = markers[0]
    253   let widget = options.widgetNode
    254   linkedDocs(doc, doc => {
    255     if (widget) options.widgetNode = widget.cloneNode(true)
    256     markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type))
    257     for (let i = 0; i < doc.linked.length; ++i)
    258       if (doc.linked[i].isParent) return
    259     primary = lst(markers)
    260   })
    261   return new SharedTextMarker(markers, primary)
    262 }
    263 
    264 export function findSharedMarkers(doc) {
    265   return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), m => m.parent)
    266 }
    267 
    268 export function copySharedMarkers(doc, markers) {
    269   for (let i = 0; i < markers.length; i++) {
    270     let marker = markers[i], pos = marker.find()
    271     let mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to)
    272     if (cmp(mFrom, mTo)) {
    273       let subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type)
    274       marker.markers.push(subMark)
    275       subMark.parent = marker
    276     }
    277   }
    278 }
    279 
    280 export function detachSharedMarkers(markers) {
    281   for (let i = 0; i < markers.length; i++) {
    282     let marker = markers[i], linked = [marker.primary.doc]
    283     linkedDocs(marker.primary.doc, d => linked.push(d))
    284     for (let j = 0; j < marker.markers.length; j++) {
    285       let subMarker = marker.markers[j]
    286       if (indexOf(linked, subMarker.doc) == -1) {
    287         subMarker.parent = null
    288         marker.markers.splice(j--, 1)
    289       }
    290     }
    291   }
    292 }