mark_text.js 11 KB

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