openrat-cms

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

position_measurement.js (28707B)


      1 import { buildLineContent, LineView } from "../line/line_data.js"
      2 import { clipPos, Pos } from "../line/pos.js"
      3 import { collapsedSpanAtEnd, heightAtLine, lineIsHidden, visualLine } from "../line/spans.js"
      4 import { getLine, lineAtHeight, lineNo, updateLineHeight } from "../line/utils_line.js"
      5 import { bidiOther, getBidiPartAt, getOrder } from "../util/bidi.js"
      6 import { chrome, android, ie, ie_version } from "../util/browser.js"
      7 import { elt, removeChildren, range, removeChildrenAndAdd } from "../util/dom.js"
      8 import { e_target } from "../util/event.js"
      9 import { hasBadZoomedRects } from "../util/feature_detection.js"
     10 import { countColumn, findFirst, isExtendingChar, scrollerGap, skipExtendingChars } from "../util/misc.js"
     11 import { updateLineForChanges } from "../display/update_line.js"
     12 
     13 import { widgetHeight } from "./widgets.js"
     14 
     15 // POSITION MEASUREMENT
     16 
     17 export function paddingTop(display) {return display.lineSpace.offsetTop}
     18 export function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight}
     19 export function paddingH(display) {
     20   if (display.cachedPaddingH) return display.cachedPaddingH
     21   let e = removeChildrenAndAdd(display.measure, elt("pre", "x"))
     22   let style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle
     23   let data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}
     24   if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data
     25   return data
     26 }
     27 
     28 export function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth }
     29 export function displayWidth(cm) {
     30   return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth
     31 }
     32 export function displayHeight(cm) {
     33   return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight
     34 }
     35 
     36 // Ensure the lineView.wrapping.heights array is populated. This is
     37 // an array of bottom offsets for the lines that make up a drawn
     38 // line. When lineWrapping is on, there might be more than one
     39 // height.
     40 function ensureLineHeights(cm, lineView, rect) {
     41   let wrapping = cm.options.lineWrapping
     42   let curWidth = wrapping && displayWidth(cm)
     43   if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) {
     44     let heights = lineView.measure.heights = []
     45     if (wrapping) {
     46       lineView.measure.width = curWidth
     47       let rects = lineView.text.firstChild.getClientRects()
     48       for (let i = 0; i < rects.length - 1; i++) {
     49         let cur = rects[i], next = rects[i + 1]
     50         if (Math.abs(cur.bottom - next.bottom) > 2)
     51           heights.push((cur.bottom + next.top) / 2 - rect.top)
     52       }
     53     }
     54     heights.push(rect.bottom - rect.top)
     55   }
     56 }
     57 
     58 // Find a line map (mapping character offsets to text nodes) and a
     59 // measurement cache for the given line number. (A line view might
     60 // contain multiple lines when collapsed ranges are present.)
     61 export function mapFromLineView(lineView, line, lineN) {
     62   if (lineView.line == line)
     63     return {map: lineView.measure.map, cache: lineView.measure.cache}
     64   for (let i = 0; i < lineView.rest.length; i++)
     65     if (lineView.rest[i] == line)
     66       return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}
     67   for (let i = 0; i < lineView.rest.length; i++)
     68     if (lineNo(lineView.rest[i]) > lineN)
     69       return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}
     70 }
     71 
     72 // Render a line into the hidden node display.externalMeasured. Used
     73 // when measurement is needed for a line that's not in the viewport.
     74 function updateExternalMeasurement(cm, line) {
     75   line = visualLine(line)
     76   let lineN = lineNo(line)
     77   let view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN)
     78   view.lineN = lineN
     79   let built = view.built = buildLineContent(cm, view)
     80   view.text = built.pre
     81   removeChildrenAndAdd(cm.display.lineMeasure, built.pre)
     82   return view
     83 }
     84 
     85 // Get a {top, bottom, left, right} box (in line-local coordinates)
     86 // for a given character.
     87 export function measureChar(cm, line, ch, bias) {
     88   return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias)
     89 }
     90 
     91 // Find a line view that corresponds to the given line number.
     92 export function findViewForLine(cm, lineN) {
     93   if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo)
     94     return cm.display.view[findViewIndex(cm, lineN)]
     95   let ext = cm.display.externalMeasured
     96   if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size)
     97     return ext
     98 }
     99 
    100 // Measurement can be split in two steps, the set-up work that
    101 // applies to the whole line, and the measurement of the actual
    102 // character. Functions like coordsChar, that need to do a lot of
    103 // measurements in a row, can thus ensure that the set-up work is
    104 // only done once.
    105 export function prepareMeasureForLine(cm, line) {
    106   let lineN = lineNo(line)
    107   let view = findViewForLine(cm, lineN)
    108   if (view && !view.text) {
    109     view = null
    110   } else if (view && view.changes) {
    111     updateLineForChanges(cm, view, lineN, getDimensions(cm))
    112     cm.curOp.forceUpdate = true
    113   }
    114   if (!view)
    115     view = updateExternalMeasurement(cm, line)
    116 
    117   let info = mapFromLineView(view, line, lineN)
    118   return {
    119     line: line, view: view, rect: null,
    120     map: info.map, cache: info.cache, before: info.before,
    121     hasHeights: false
    122   }
    123 }
    124 
    125 // Given a prepared measurement object, measures the position of an
    126 // actual character (or fetches it from the cache).
    127 export function measureCharPrepared(cm, prepared, ch, bias, varHeight) {
    128   if (prepared.before) ch = -1
    129   let key = ch + (bias || ""), found
    130   if (prepared.cache.hasOwnProperty(key)) {
    131     found = prepared.cache[key]
    132   } else {
    133     if (!prepared.rect)
    134       prepared.rect = prepared.view.text.getBoundingClientRect()
    135     if (!prepared.hasHeights) {
    136       ensureLineHeights(cm, prepared.view, prepared.rect)
    137       prepared.hasHeights = true
    138     }
    139     found = measureCharInner(cm, prepared, ch, bias)
    140     if (!found.bogus) prepared.cache[key] = found
    141   }
    142   return {left: found.left, right: found.right,
    143           top: varHeight ? found.rtop : found.top,
    144           bottom: varHeight ? found.rbottom : found.bottom}
    145 }
    146 
    147 let nullRect = {left: 0, right: 0, top: 0, bottom: 0}
    148 
    149 export function nodeAndOffsetInLineMap(map, ch, bias) {
    150   let node, start, end, collapse, mStart, mEnd
    151   // First, search the line map for the text node corresponding to,
    152   // or closest to, the target character.
    153   for (let i = 0; i < map.length; i += 3) {
    154     mStart = map[i]
    155     mEnd = map[i + 1]
    156     if (ch < mStart) {
    157       start = 0; end = 1
    158       collapse = "left"
    159     } else if (ch < mEnd) {
    160       start = ch - mStart
    161       end = start + 1
    162     } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) {
    163       end = mEnd - mStart
    164       start = end - 1
    165       if (ch >= mEnd) collapse = "right"
    166     }
    167     if (start != null) {
    168       node = map[i + 2]
    169       if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right"))
    170         collapse = bias
    171       if (bias == "left" && start == 0)
    172         while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) {
    173           node = map[(i -= 3) + 2]
    174           collapse = "left"
    175         }
    176       if (bias == "right" && start == mEnd - mStart)
    177         while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) {
    178           node = map[(i += 3) + 2]
    179           collapse = "right"
    180         }
    181       break
    182     }
    183   }
    184   return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd}
    185 }
    186 
    187 function getUsefulRect(rects, bias) {
    188   let rect = nullRect
    189   if (bias == "left") for (let i = 0; i < rects.length; i++) {
    190     if ((rect = rects[i]).left != rect.right) break
    191   } else for (let i = rects.length - 1; i >= 0; i--) {
    192     if ((rect = rects[i]).left != rect.right) break
    193   }
    194   return rect
    195 }
    196 
    197 function measureCharInner(cm, prepared, ch, bias) {
    198   let place = nodeAndOffsetInLineMap(prepared.map, ch, bias)
    199   let node = place.node, start = place.start, end = place.end, collapse = place.collapse
    200 
    201   let rect
    202   if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates.
    203     for (let i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned
    204       while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start
    205       while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end
    206       if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart)
    207         rect = node.parentNode.getBoundingClientRect()
    208       else
    209         rect = getUsefulRect(range(node, start, end).getClientRects(), bias)
    210       if (rect.left || rect.right || start == 0) break
    211       end = start
    212       start = start - 1
    213       collapse = "right"
    214     }
    215     if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect)
    216   } else { // If it is a widget, simply get the box for the whole widget.
    217     if (start > 0) collapse = bias = "right"
    218     let rects
    219     if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1)
    220       rect = rects[bias == "right" ? rects.length - 1 : 0]
    221     else
    222       rect = node.getBoundingClientRect()
    223   }
    224   if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) {
    225     let rSpan = node.parentNode.getClientRects()[0]
    226     if (rSpan)
    227       rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}
    228     else
    229       rect = nullRect
    230   }
    231 
    232   let rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top
    233   let mid = (rtop + rbot) / 2
    234   let heights = prepared.view.measure.heights
    235   let i = 0
    236   for (; i < heights.length - 1; i++)
    237     if (mid < heights[i]) break
    238   let top = i ? heights[i - 1] : 0, bot = heights[i]
    239   let result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left,
    240                 right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left,
    241                 top: top, bottom: bot}
    242   if (!rect.left && !rect.right) result.bogus = true
    243   if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot }
    244 
    245   return result
    246 }
    247 
    248 // Work around problem with bounding client rects on ranges being
    249 // returned incorrectly when zoomed on IE10 and below.
    250 function maybeUpdateRectForZooming(measure, rect) {
    251   if (!window.screen || screen.logicalXDPI == null ||
    252       screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure))
    253     return rect
    254   let scaleX = screen.logicalXDPI / screen.deviceXDPI
    255   let scaleY = screen.logicalYDPI / screen.deviceYDPI
    256   return {left: rect.left * scaleX, right: rect.right * scaleX,
    257           top: rect.top * scaleY, bottom: rect.bottom * scaleY}
    258 }
    259 
    260 export function clearLineMeasurementCacheFor(lineView) {
    261   if (lineView.measure) {
    262     lineView.measure.cache = {}
    263     lineView.measure.heights = null
    264     if (lineView.rest) for (let i = 0; i < lineView.rest.length; i++)
    265       lineView.measure.caches[i] = {}
    266   }
    267 }
    268 
    269 export function clearLineMeasurementCache(cm) {
    270   cm.display.externalMeasure = null
    271   removeChildren(cm.display.lineMeasure)
    272   for (let i = 0; i < cm.display.view.length; i++)
    273     clearLineMeasurementCacheFor(cm.display.view[i])
    274 }
    275 
    276 export function clearCaches(cm) {
    277   clearLineMeasurementCache(cm)
    278   cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null
    279   if (!cm.options.lineWrapping) cm.display.maxLineChanged = true
    280   cm.display.lineNumChars = null
    281 }
    282 
    283 function pageScrollX() {
    284   // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=489206
    285   // which causes page_Offset and bounding client rects to use
    286   // different reference viewports and invalidate our calculations.
    287   if (chrome && android) return -(document.body.getBoundingClientRect().left - parseInt(getComputedStyle(document.body).marginLeft))
    288   return window.pageXOffset || (document.documentElement || document.body).scrollLeft
    289 }
    290 function pageScrollY() {
    291   if (chrome && android) return -(document.body.getBoundingClientRect().top - parseInt(getComputedStyle(document.body).marginTop))
    292   return window.pageYOffset || (document.documentElement || document.body).scrollTop
    293 }
    294 
    295 function widgetTopHeight(lineObj) {
    296   let height = 0
    297   if (lineObj.widgets) for (let i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above)
    298     height += widgetHeight(lineObj.widgets[i])
    299   return height
    300 }
    301 
    302 // Converts a {top, bottom, left, right} box from line-local
    303 // coordinates into another coordinate system. Context may be one of
    304 // "line", "div" (display.lineDiv), "local"./null (editor), "window",
    305 // or "page".
    306 export function intoCoordSystem(cm, lineObj, rect, context, includeWidgets) {
    307   if (!includeWidgets) {
    308     let height = widgetTopHeight(lineObj)
    309     rect.top += height; rect.bottom += height
    310   }
    311   if (context == "line") return rect
    312   if (!context) context = "local"
    313   let yOff = heightAtLine(lineObj)
    314   if (context == "local") yOff += paddingTop(cm.display)
    315   else yOff -= cm.display.viewOffset
    316   if (context == "page" || context == "window") {
    317     let lOff = cm.display.lineSpace.getBoundingClientRect()
    318     yOff += lOff.top + (context == "window" ? 0 : pageScrollY())
    319     let xOff = lOff.left + (context == "window" ? 0 : pageScrollX())
    320     rect.left += xOff; rect.right += xOff
    321   }
    322   rect.top += yOff; rect.bottom += yOff
    323   return rect
    324 }
    325 
    326 // Coverts a box from "div" coords to another coordinate system.
    327 // Context may be "window", "page", "div", or "local"./null.
    328 export function fromCoordSystem(cm, coords, context) {
    329   if (context == "div") return coords
    330   let left = coords.left, top = coords.top
    331   // First move into "page" coordinate system
    332   if (context == "page") {
    333     left -= pageScrollX()
    334     top -= pageScrollY()
    335   } else if (context == "local" || !context) {
    336     let localBox = cm.display.sizer.getBoundingClientRect()
    337     left += localBox.left
    338     top += localBox.top
    339   }
    340 
    341   let lineSpaceBox = cm.display.lineSpace.getBoundingClientRect()
    342   return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}
    343 }
    344 
    345 export function charCoords(cm, pos, context, lineObj, bias) {
    346   if (!lineObj) lineObj = getLine(cm.doc, pos.line)
    347   return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context)
    348 }
    349 
    350 // Returns a box for a given cursor position, which may have an
    351 // 'other' property containing the position of the secondary cursor
    352 // on a bidi boundary.
    353 // A cursor Pos(line, char, "before") is on the same visual line as `char - 1`
    354 // and after `char - 1` in writing order of `char - 1`
    355 // A cursor Pos(line, char, "after") is on the same visual line as `char`
    356 // and before `char` in writing order of `char`
    357 // Examples (upper-case letters are RTL, lower-case are LTR):
    358 //     Pos(0, 1, ...)
    359 //     before   after
    360 // ab     a|b     a|b
    361 // aB     a|B     aB|
    362 // Ab     |Ab     A|b
    363 // AB     B|A     B|A
    364 // Every position after the last character on a line is considered to stick
    365 // to the last character on the line.
    366 export function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) {
    367   lineObj = lineObj || getLine(cm.doc, pos.line)
    368   if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj)
    369   function get(ch, right) {
    370     let m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight)
    371     if (right) m.left = m.right; else m.right = m.left
    372     return intoCoordSystem(cm, lineObj, m, context)
    373   }
    374   let order = getOrder(lineObj, cm.doc.direction), ch = pos.ch, sticky = pos.sticky
    375   if (ch >= lineObj.text.length) {
    376     ch = lineObj.text.length
    377     sticky = "before"
    378   } else if (ch <= 0) {
    379     ch = 0
    380     sticky = "after"
    381   }
    382   if (!order) return get(sticky == "before" ? ch - 1 : ch, sticky == "before")
    383 
    384   function getBidi(ch, partPos, invert) {
    385     let part = order[partPos], right = part.level == 1
    386     return get(invert ? ch - 1 : ch, right != invert)
    387   }
    388   let partPos = getBidiPartAt(order, ch, sticky)
    389   let other = bidiOther
    390   let val = getBidi(ch, partPos, sticky == "before")
    391   if (other != null) val.other = getBidi(ch, other, sticky != "before")
    392   return val
    393 }
    394 
    395 // Used to cheaply estimate the coordinates for a position. Used for
    396 // intermediate scroll updates.
    397 export function estimateCoords(cm, pos) {
    398   let left = 0
    399   pos = clipPos(cm.doc, pos)
    400   if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch
    401   let lineObj = getLine(cm.doc, pos.line)
    402   let top = heightAtLine(lineObj) + paddingTop(cm.display)
    403   return {left: left, right: left, top: top, bottom: top + lineObj.height}
    404 }
    405 
    406 // Positions returned by coordsChar contain some extra information.
    407 // xRel is the relative x position of the input coordinates compared
    408 // to the found position (so xRel > 0 means the coordinates are to
    409 // the right of the character position, for example). When outside
    410 // is true, that means the coordinates lie outside the line's
    411 // vertical range.
    412 function PosWithInfo(line, ch, sticky, outside, xRel) {
    413   let pos = Pos(line, ch, sticky)
    414   pos.xRel = xRel
    415   if (outside) pos.outside = true
    416   return pos
    417 }
    418 
    419 // Compute the character position closest to the given coordinates.
    420 // Input must be lineSpace-local ("div" coordinate system).
    421 export function coordsChar(cm, x, y) {
    422   let doc = cm.doc
    423   y += cm.display.viewOffset
    424   if (y < 0) return PosWithInfo(doc.first, 0, null, true, -1)
    425   let lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1
    426   if (lineN > last)
    427     return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, true, 1)
    428   if (x < 0) x = 0
    429 
    430   let lineObj = getLine(doc, lineN)
    431   for (;;) {
    432     let found = coordsCharInner(cm, lineObj, lineN, x, y)
    433     let merged = collapsedSpanAtEnd(lineObj)
    434     let mergedPos = merged && merged.find(0, true)
    435     if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))
    436       lineN = lineNo(lineObj = mergedPos.to.line)
    437     else
    438       return found
    439   }
    440 }
    441 
    442 function wrappedLineExtent(cm, lineObj, preparedMeasure, y) {
    443   y -= widgetTopHeight(lineObj)
    444   let end = lineObj.text.length
    445   let begin = findFirst(ch => measureCharPrepared(cm, preparedMeasure, ch - 1).bottom <= y, end, 0)
    446   end = findFirst(ch => measureCharPrepared(cm, preparedMeasure, ch).top > y, begin, end)
    447   return {begin, end}
    448 }
    449 
    450 export function wrappedLineExtentChar(cm, lineObj, preparedMeasure, target) {
    451   if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj)
    452   let targetTop = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, target), "line").top
    453   return wrappedLineExtent(cm, lineObj, preparedMeasure, targetTop)
    454 }
    455 
    456 // Returns true if the given side of a box is after the given
    457 // coordinates, in top-to-bottom, left-to-right order.
    458 function boxIsAfter(box, x, y, left) {
    459   return box.bottom <= y ? false : box.top > y ? true : (left ? box.left : box.right) > x
    460 }
    461 
    462 function coordsCharInner(cm, lineObj, lineNo, x, y) {
    463   // Move y into line-local coordinate space
    464   y -= heightAtLine(lineObj)
    465   let preparedMeasure = prepareMeasureForLine(cm, lineObj)
    466   // When directly calling `measureCharPrepared`, we have to adjust
    467   // for the widgets at this line.
    468   let widgetHeight = widgetTopHeight(lineObj)
    469   let begin = 0, end = lineObj.text.length, ltr = true
    470 
    471   let order = getOrder(lineObj, cm.doc.direction)
    472   // If the line isn't plain left-to-right text, first figure out
    473   // which bidi section the coordinates fall into.
    474   if (order) {
    475     let part = (cm.options.lineWrapping ? coordsBidiPartWrapped : coordsBidiPart)
    476                  (cm, lineObj, lineNo, preparedMeasure, order, x, y)
    477     ltr = part.level != 1
    478     // The awkward -1 offsets are needed because findFirst (called
    479     // on these below) will treat its first bound as inclusive,
    480     // second as exclusive, but we want to actually address the
    481     // characters in the part's range
    482     begin = ltr ? part.from : part.to - 1
    483     end = ltr ? part.to : part.from - 1
    484   }
    485 
    486   // A binary search to find the first character whose bounding box
    487   // starts after the coordinates. If we run across any whose box wrap
    488   // the coordinates, store that.
    489   let chAround = null, boxAround = null
    490   let ch = findFirst(ch => {
    491     let box = measureCharPrepared(cm, preparedMeasure, ch)
    492     box.top += widgetHeight; box.bottom += widgetHeight
    493     if (!boxIsAfter(box, x, y, false)) return false
    494     if (box.top <= y && box.left <= x) {
    495       chAround = ch
    496       boxAround = box
    497     }
    498     return true
    499   }, begin, end)
    500 
    501   let baseX, sticky, outside = false
    502   // If a box around the coordinates was found, use that
    503   if (boxAround) {
    504     // Distinguish coordinates nearer to the left or right side of the box
    505     let atLeft = x - boxAround.left < boxAround.right - x, atStart = atLeft == ltr
    506     ch = chAround + (atStart ? 0 : 1)
    507     sticky = atStart ? "after" : "before"
    508     baseX = atLeft ? boxAround.left : boxAround.right
    509   } else {
    510     // (Adjust for extended bound, if necessary.)
    511     if (!ltr && (ch == end || ch == begin)) ch++
    512     // To determine which side to associate with, get the box to the
    513     // left of the character and compare it's vertical position to the
    514     // coordinates
    515     sticky = ch == 0 ? "after" : ch == lineObj.text.length ? "before" :
    516       (measureCharPrepared(cm, preparedMeasure, ch - (ltr ? 1 : 0)).bottom + widgetHeight <= y) == ltr ?
    517       "after" : "before"
    518     // Now get accurate coordinates for this place, in order to get a
    519     // base X position
    520     let coords = cursorCoords(cm, Pos(lineNo, ch, sticky), "line", lineObj, preparedMeasure)
    521     baseX = coords.left
    522     outside = y < coords.top || y >= coords.bottom
    523   }
    524 
    525   ch = skipExtendingChars(lineObj.text, ch, 1)
    526   return PosWithInfo(lineNo, ch, sticky, outside, x - baseX)
    527 }
    528 
    529 function coordsBidiPart(cm, lineObj, lineNo, preparedMeasure, order, x, y) {
    530   // Bidi parts are sorted left-to-right, and in a non-line-wrapping
    531   // situation, we can take this ordering to correspond to the visual
    532   // ordering. This finds the first part whose end is after the given
    533   // coordinates.
    534   let index = findFirst(i => {
    535     let part = order[i], ltr = part.level != 1
    536     return boxIsAfter(cursorCoords(cm, Pos(lineNo, ltr ? part.to : part.from, ltr ? "before" : "after"),
    537                                    "line", lineObj, preparedMeasure), x, y, true)
    538   }, 0, order.length - 1)
    539   let part = order[index]
    540   // If this isn't the first part, the part's start is also after
    541   // the coordinates, and the coordinates aren't on the same line as
    542   // that start, move one part back.
    543   if (index > 0) {
    544     let ltr = part.level != 1
    545     let start = cursorCoords(cm, Pos(lineNo, ltr ? part.from : part.to, ltr ? "after" : "before"),
    546                              "line", lineObj, preparedMeasure)
    547     if (boxIsAfter(start, x, y, true) && start.top > y)
    548       part = order[index - 1]
    549   }
    550   return part
    551 }
    552 
    553 function coordsBidiPartWrapped(cm, lineObj, _lineNo, preparedMeasure, order, x, y) {
    554   // In a wrapped line, rtl text on wrapping boundaries can do things
    555   // that don't correspond to the ordering in our `order` array at
    556   // all, so a binary search doesn't work, and we want to return a
    557   // part that only spans one line so that the binary search in
    558   // coordsCharInner is safe. As such, we first find the extent of the
    559   // wrapped line, and then do a flat search in which we discard any
    560   // spans that aren't on the line.
    561   let {begin, end} = wrappedLineExtent(cm, lineObj, preparedMeasure, y)
    562   if (/\s/.test(lineObj.text.charAt(end - 1))) end--
    563   let part = null, closestDist = null
    564   for (let i = 0; i < order.length; i++) {
    565     let p = order[i]
    566     if (p.from >= end || p.to <= begin) continue
    567     let ltr = p.level != 1
    568     let endX = measureCharPrepared(cm, preparedMeasure, ltr ? Math.min(end, p.to) - 1 : Math.max(begin, p.from)).right
    569     // Weigh against spans ending before this, so that they are only
    570     // picked if nothing ends after
    571     let dist = endX < x ? x - endX + 1e9 : endX - x
    572     if (!part || closestDist > dist) {
    573       part = p
    574       closestDist = dist
    575     }
    576   }
    577   if (!part) part = order[order.length - 1]
    578   // Clip the part to the wrapped line.
    579   if (part.from < begin) part = {from: begin, to: part.to, level: part.level}
    580   if (part.to > end) part = {from: part.from, to: end, level: part.level}
    581   return part
    582 }
    583 
    584 let measureText
    585 // Compute the default text height.
    586 export function textHeight(display) {
    587   if (display.cachedTextHeight != null) return display.cachedTextHeight
    588   if (measureText == null) {
    589     measureText = elt("pre")
    590     // Measure a bunch of lines, for browsers that compute
    591     // fractional heights.
    592     for (let i = 0; i < 49; ++i) {
    593       measureText.appendChild(document.createTextNode("x"))
    594       measureText.appendChild(elt("br"))
    595     }
    596     measureText.appendChild(document.createTextNode("x"))
    597   }
    598   removeChildrenAndAdd(display.measure, measureText)
    599   let height = measureText.offsetHeight / 50
    600   if (height > 3) display.cachedTextHeight = height
    601   removeChildren(display.measure)
    602   return height || 1
    603 }
    604 
    605 // Compute the default character width.
    606 export function charWidth(display) {
    607   if (display.cachedCharWidth != null) return display.cachedCharWidth
    608   let anchor = elt("span", "xxxxxxxxxx")
    609   let pre = elt("pre", [anchor])
    610   removeChildrenAndAdd(display.measure, pre)
    611   let rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10
    612   if (width > 2) display.cachedCharWidth = width
    613   return width || 10
    614 }
    615 
    616 // Do a bulk-read of the DOM positions and sizes needed to draw the
    617 // view, so that we don't interleave reading and writing to the DOM.
    618 export function getDimensions(cm) {
    619   let d = cm.display, left = {}, width = {}
    620   let gutterLeft = d.gutters.clientLeft
    621   for (let n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) {
    622     left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft
    623     width[cm.options.gutters[i]] = n.clientWidth
    624   }
    625   return {fixedPos: compensateForHScroll(d),
    626           gutterTotalWidth: d.gutters.offsetWidth,
    627           gutterLeft: left,
    628           gutterWidth: width,
    629           wrapperWidth: d.wrapper.clientWidth}
    630 }
    631 
    632 // Computes display.scroller.scrollLeft + display.gutters.offsetWidth,
    633 // but using getBoundingClientRect to get a sub-pixel-accurate
    634 // result.
    635 export function compensateForHScroll(display) {
    636   return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left
    637 }
    638 
    639 // Returns a function that estimates the height of a line, to use as
    640 // first approximation until the line becomes visible (and is thus
    641 // properly measurable).
    642 export function estimateHeight(cm) {
    643   let th = textHeight(cm.display), wrapping = cm.options.lineWrapping
    644   let perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3)
    645   return line => {
    646     if (lineIsHidden(cm.doc, line)) return 0
    647 
    648     let widgetsHeight = 0
    649     if (line.widgets) for (let i = 0; i < line.widgets.length; i++) {
    650       if (line.widgets[i].height) widgetsHeight += line.widgets[i].height
    651     }
    652 
    653     if (wrapping)
    654       return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th
    655     else
    656       return widgetsHeight + th
    657   }
    658 }
    659 
    660 export function estimateLineHeights(cm) {
    661   let doc = cm.doc, est = estimateHeight(cm)
    662   doc.iter(line => {
    663     let estHeight = est(line)
    664     if (estHeight != line.height) updateLineHeight(line, estHeight)
    665   })
    666 }
    667 
    668 // Given a mouse event, find the corresponding position. If liberal
    669 // is false, it checks whether a gutter or scrollbar was clicked,
    670 // and returns null if it was. forRect is used by rectangular
    671 // selections, and tries to estimate a character position even for
    672 // coordinates beyond the right of the text.
    673 export function posFromMouse(cm, e, liberal, forRect) {
    674   let display = cm.display
    675   if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null
    676 
    677   let x, y, space = display.lineSpace.getBoundingClientRect()
    678   // Fails unpredictably on IE[67] when mouse is dragged around quickly.
    679   try { x = e.clientX - space.left; y = e.clientY - space.top }
    680   catch (e) { return null }
    681   let coords = coordsChar(cm, x, y), line
    682   if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) {
    683     let colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length
    684     coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff))
    685   }
    686   return coords
    687 }
    688 
    689 // Find the view element corresponding to a given line. Return null
    690 // when the line isn't visible.
    691 export function findViewIndex(cm, n) {
    692   if (n >= cm.display.viewTo) return null
    693   n -= cm.display.viewFrom
    694   if (n < 0) return null
    695   let view = cm.display.view
    696   for (let i = 0; i < view.length; i++) {
    697     n -= view[i].size
    698     if (n < 0) return i
    699   }
    700 }