mark_text.min.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 }