a753d2c39f7b481d92af38b17394748cc573519063970237f5c37bfb55f4270a693b05b82cdba0b3f9dfc59974ad659244f40d1582bdb0c2ab605ec9efc7ae 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import { indexOf, lst } from "../util/misc.js"
  2. import { cmp } from "./pos.js"
  3. import { sawCollapsedSpans } from "./saw_special_spans.js"
  4. import { getLine, isLine, lineNo } from "./utils_line.js"
  5. // TEXTMARKER SPANS
  6. export function MarkedSpan(marker, from, to) {
  7. this.marker = marker
  8. this.from = from; this.to = to
  9. }
  10. // Search an array of spans for a span matching the given marker.
  11. export function getMarkedSpanFor(spans, marker) {
  12. if (spans) for (let i = 0; i < spans.length; ++i) {
  13. let span = spans[i]
  14. if (span.marker == marker) return span
  15. }
  16. }
  17. // Remove a span from an array, returning undefined if no spans are
  18. // left (we don't store arrays for lines without spans).
  19. export function removeMarkedSpan(spans, span) {
  20. let r
  21. for (let i = 0; i < spans.length; ++i)
  22. if (spans[i] != span) (r || (r = [])).push(spans[i])
  23. return r
  24. }
  25. // Add a span to a line.
  26. export function addMarkedSpan(line, span, op) {
  27. let inThisOp = op && window.WeakSet && (op.markedSpans || (op.markedSpans = new WeakSet))
  28. if (inThisOp && line.markedSpans && inThisOp.has(line.markedSpans)) {
  29. line.markedSpans.push(span)
  30. } else {
  31. line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]
  32. if (inThisOp) inThisOp.add(line.markedSpans)
  33. }
  34. span.marker.attachLine(line)
  35. }
  36. // Used for the algorithm that adjusts markers for a change in the
  37. // document. These functions cut an array of spans at a given
  38. // character position, returning an array of remaining chunks (or
  39. // undefined if nothing remains).
  40. function markedSpansBefore(old, startCh, isInsert) {
  41. let nw
  42. if (old) for (let i = 0; i < old.length; ++i) {
  43. let span = old[i], marker = span.marker
  44. let startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh)
  45. if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) {
  46. let endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh)
  47. ;(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to))
  48. }
  49. }
  50. return nw
  51. }
  52. function markedSpansAfter(old, endCh, isInsert) {
  53. let nw
  54. if (old) for (let i = 0; i < old.length; ++i) {
  55. let span = old[i], marker = span.marker
  56. let endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh)
  57. if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) {
  58. let startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh)
  59. ;(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh,
  60. span.to == null ? null : span.to - endCh))
  61. }
  62. }
  63. return nw
  64. }
  65. // Given a change object, compute the new set of marker spans that
  66. // cover the line in which the change took place. Removes spans
  67. // entirely within the change, reconnects spans belonging to the
  68. // same marker that appear on both sides of the change, and cuts off
  69. // spans partially within the change. Returns an array of span
  70. // arrays with one element for each line in (after) the change.
  71. export function stretchSpansOverChange(doc, change) {
  72. if (change.full) return null
  73. let oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans
  74. let oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans
  75. if (!oldFirst && !oldLast) return null
  76. let startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0
  77. // Get the spans that 'stick out' on both sides
  78. let first = markedSpansBefore(oldFirst, startCh, isInsert)
  79. let last = markedSpansAfter(oldLast, endCh, isInsert)
  80. // Next, merge those two ends
  81. let sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0)
  82. if (first) {
  83. // Fix up .to properties of first
  84. for (let i = 0; i < first.length; ++i) {
  85. let span = first[i]
  86. if (span.to == null) {
  87. let found = getMarkedSpanFor(last, span.marker)
  88. if (!found) span.to = startCh
  89. else if (sameLine) span.to = found.to == null ? null : found.to + offset
  90. }
  91. }
  92. }
  93. if (last) {
  94. // Fix up .from in last (or move them into first in case of sameLine)
  95. for (let i = 0; i < last.length; ++i) {
  96. let span = last[i]
  97. if (span.to != null) span.to += offset
  98. if (span.from == null) {
  99. let found = getMarkedSpanFor(first, span.marker)
  100. if (!found) {
  101. span.from = offset
  102. if (sameLine) (first || (first = [])).push(span)
  103. }
  104. } else {
  105. span.from += offset
  106. if (sameLine) (first || (first = [])).push(span)
  107. }
  108. }
  109. }
  110. // Make sure we didn't create any zero-length spans
  111. if (first) first = clearEmptySpans(first)
  112. if (last && last != first) last = clearEmptySpans(last)
  113. let newMarkers = [first]
  114. if (!sameLine) {
  115. // Fill gap with whole-line-spans
  116. let gap = change.text.length - 2, gapMarkers
  117. if (gap > 0 && first)
  118. for (let i = 0; i < first.length; ++i)
  119. if (first[i].to == null)
  120. (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null))
  121. for (let i = 0; i < gap; ++i)
  122. newMarkers.push(gapMarkers)
  123. newMarkers.push(last)
  124. }
  125. return newMarkers
  126. }
  127. // Remove spans that are empty and don't have a clearWhenEmpty
  128. // option of false.
  129. function clearEmptySpans(spans) {
  130. for (let i = 0; i < spans.length; ++i) {
  131. let span = spans[i]
  132. if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false)
  133. spans.splice(i--, 1)
  134. }
  135. if (!spans.length) return null
  136. return spans
  137. }
  138. // Used to 'clip' out readOnly ranges when making a change.
  139. export function removeReadOnlyRanges(doc, from, to) {
  140. let markers = null
  141. doc.iter(from.line, to.line + 1, line => {
  142. if (line.markedSpans) for (let i = 0; i < line.markedSpans.length; ++i) {
  143. let mark = line.markedSpans[i].marker
  144. if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))
  145. (markers || (markers = [])).push(mark)
  146. }
  147. })
  148. if (!markers) return null
  149. let parts = [{from: from, to: to}]
  150. for (let i = 0; i < markers.length; ++i) {
  151. let mk = markers[i], m = mk.find(0)
  152. for (let j = 0; j < parts.length; ++j) {
  153. let p = parts[j]
  154. if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue
  155. let newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to)
  156. if (dfrom < 0 || !mk.inclusiveLeft && !dfrom)
  157. newParts.push({from: p.from, to: m.from})
  158. if (dto > 0 || !mk.inclusiveRight && !dto)
  159. newParts.push({from: m.to, to: p.to})
  160. parts.splice.apply(parts, newParts)
  161. j += newParts.length - 3
  162. }
  163. }
  164. return parts
  165. }
  166. // Connect or disconnect spans from a line.
  167. export function detachMarkedSpans(line) {
  168. let spans = line.markedSpans
  169. if (!spans) return
  170. for (let i = 0; i < spans.length; ++i)
  171. spans[i].marker.detachLine(line)
  172. line.markedSpans = null
  173. }
  174. export function attachMarkedSpans(line, spans) {
  175. if (!spans) return
  176. for (let i = 0; i < spans.length; ++i)
  177. spans[i].marker.attachLine(line)
  178. line.markedSpans = spans
  179. }
  180. // Helpers used when computing which overlapping collapsed span
  181. // counts as the larger one.
  182. function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 }
  183. function extraRight(marker) { return marker.inclusiveRight ? 1 : 0 }
  184. // Returns a number indicating which of two overlapping collapsed
  185. // spans is larger (and thus includes the other). Falls back to
  186. // comparing ids when the spans cover exactly the same range.
  187. export function compareCollapsedMarkers(a, b) {
  188. let lenDiff = a.lines.length - b.lines.length
  189. if (lenDiff != 0) return lenDiff
  190. let aPos = a.find(), bPos = b.find()
  191. let fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b)
  192. if (fromCmp) return -fromCmp
  193. let toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b)
  194. if (toCmp) return toCmp
  195. return b.id - a.id
  196. }
  197. // Find out whether a line ends or starts in a collapsed span. If
  198. // so, return the marker for that span.
  199. function collapsedSpanAtSide(line, start) {
  200. let sps = sawCollapsedSpans && line.markedSpans, found
  201. if (sps) for (let sp, i = 0; i < sps.length; ++i) {
  202. sp = sps[i]
  203. if (sp.marker.collapsed && (start ? sp.from : sp.to) == null &&
  204. (!found || compareCollapsedMarkers(found, sp.marker) < 0))
  205. found = sp.marker
  206. }
  207. return found
  208. }
  209. export function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) }
  210. export function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) }
  211. export function collapsedSpanAround(line, ch) {
  212. let sps = sawCollapsedSpans && line.markedSpans, found
  213. if (sps) for (let i = 0; i < sps.length; ++i) {
  214. let sp = sps[i]
  215. if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) &&
  216. (!found || compareCollapsedMarkers(found, sp.marker) < 0)) found = sp.marker
  217. }
  218. return found
  219. }
  220. // Test whether there exists a collapsed span that partially
  221. // overlaps (covers the start or end, but not both) of a new span.
  222. // Such overlap is not allowed.
  223. export function conflictingCollapsedRange(doc, lineNo, from, to, marker) {
  224. let line = getLine(doc, lineNo)
  225. let sps = sawCollapsedSpans && line.markedSpans
  226. if (sps) for (let i = 0; i < sps.length; ++i) {
  227. let sp = sps[i]
  228. if (!sp.marker.collapsed) continue
  229. let found = sp.marker.find(0)
  230. let fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker)
  231. let toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker)
  232. if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue
  233. if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) ||
  234. fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0))
  235. return true
  236. }
  237. }
  238. // A visual line is a line as drawn on the screen. Folding, for
  239. // example, can cause multiple logical lines to appear on the same
  240. // visual line. This finds the start of the visual line that the
  241. // given line is part of (usually that is the line itself).
  242. export function visualLine(line) {
  243. let merged
  244. while (merged = collapsedSpanAtStart(line))
  245. line = merged.find(-1, true).line
  246. return line
  247. }
  248. export function visualLineEnd(line) {
  249. let merged
  250. while (merged = collapsedSpanAtEnd(line))
  251. line = merged.find(1, true).line
  252. return line
  253. }
  254. // Returns an array of logical lines that continue the visual line
  255. // started by the argument, or undefined if there are no such lines.
  256. export function visualLineContinued(line) {
  257. let merged, lines
  258. while (merged = collapsedSpanAtEnd(line)) {
  259. line = merged.find(1, true).line
  260. ;(lines || (lines = [])).push(line)
  261. }
  262. return lines
  263. }
  264. // Get the line number of the start of the visual line that the
  265. // given line number is part of.
  266. export function visualLineNo(doc, lineN) {
  267. let line = getLine(doc, lineN), vis = visualLine(line)
  268. if (line == vis) return lineN
  269. return lineNo(vis)
  270. }
  271. // Get the line number of the start of the next visual line after
  272. // the given line.
  273. export function visualLineEndNo(doc, lineN) {
  274. if (lineN > doc.lastLine()) return lineN
  275. let line = getLine(doc, lineN), merged
  276. if (!lineIsHidden(doc, line)) return lineN
  277. while (merged = collapsedSpanAtEnd(line))
  278. line = merged.find(1, true).line
  279. return lineNo(line) + 1
  280. }
  281. // Compute whether a line is hidden. Lines count as hidden when they
  282. // are part of a visual line that starts with another line, or when
  283. // they are entirely covered by collapsed, non-widget span.
  284. export function lineIsHidden(doc, line) {
  285. let sps = sawCollapsedSpans && line.markedSpans
  286. if (sps) for (let sp, i = 0; i < sps.length; ++i) {
  287. sp = sps[i]
  288. if (!sp.marker.collapsed) continue
  289. if (sp.from == null) return true
  290. if (sp.marker.widgetNode) continue
  291. if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp))
  292. return true
  293. }
  294. }
  295. function lineIsHiddenInner(doc, line, span) {
  296. if (span.to == null) {
  297. let end = span.marker.find(1, true)
  298. return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker))
  299. }
  300. if (span.marker.inclusiveRight && span.to == line.text.length)
  301. return true
  302. for (let sp, i = 0; i < line.markedSpans.length; ++i) {
  303. sp = line.markedSpans[i]
  304. if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to &&
  305. (sp.to == null || sp.to != span.from) &&
  306. (sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
  307. lineIsHiddenInner(doc, line, sp)) return true
  308. }
  309. }
  310. // Find the height above the given line.
  311. export function heightAtLine(lineObj) {
  312. lineObj = visualLine(lineObj)
  313. let h = 0, chunk = lineObj.parent
  314. for (let i = 0; i < chunk.lines.length; ++i) {
  315. let line = chunk.lines[i]
  316. if (line == lineObj) break
  317. else h += line.height
  318. }
  319. for (let p = chunk.parent; p; chunk = p, p = chunk.parent) {
  320. for (let i = 0; i < p.children.length; ++i) {
  321. let cur = p.children[i]
  322. if (cur == chunk) break
  323. else h += cur.height
  324. }
  325. }
  326. return h
  327. }
  328. // Compute the character length of a line, taking into account
  329. // collapsed ranges (see markText) that might hide parts, and join
  330. // other lines onto it.
  331. export function lineLength(line) {
  332. if (line.height == 0) return 0
  333. let len = line.text.length, merged, cur = line
  334. while (merged = collapsedSpanAtStart(cur)) {
  335. let found = merged.find(0, true)
  336. cur = found.from.line
  337. len += found.from.ch - found.to.ch
  338. }
  339. cur = line
  340. while (merged = collapsedSpanAtEnd(cur)) {
  341. let found = merged.find(0, true)
  342. len -= cur.text.length - found.from.ch
  343. cur = found.to.line
  344. len += cur.text.length - found.to.ch
  345. }
  346. return len
  347. }
  348. // Find the longest line in the document.
  349. export function findMaxLine(cm) {
  350. let d = cm.display, doc = cm.doc
  351. d.maxLine = getLine(doc, doc.first)
  352. d.maxLineLength = lineLength(d.maxLine)
  353. d.maxLineChanged = true
  354. doc.iter(line => {
  355. let len = lineLength(line)
  356. if (len > d.maxLineLength) {
  357. d.maxLineLength = len
  358. d.maxLine = line
  359. }
  360. })
  361. }