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