722369c9fdccf5f3b4eab60874323fc517626e614bd354f97407ab8e504937a0f6075491a4bb75fdf056f8df6992ddb9b3642f23c9ef15fdf6aa919d4e174a 28 KB

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