openrat-cms

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

spans.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 }