update_display.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import { sawCollapsedSpans } from "../line/saw_special_spans.js"
  2. import { heightAtLine, visualLineEndNo, visualLineNo } from "../line/spans.js"
  3. import { getLine, lineNumberFor } from "../line/utils_line.js"
  4. import { displayHeight, displayWidth, getDimensions, paddingVert, scrollGap } from "../measurement/position_measurement.js"
  5. import { mac, webkit } from "../util/browser.js"
  6. import { activeElt, removeChildren, contains } from "../util/dom.js"
  7. import { hasHandler, signal } from "../util/event.js"
  8. import { indexOf } from "../util/misc.js"
  9. import { buildLineElement, updateLineForChanges } from "./update_line.js"
  10. import { startWorker } from "./highlight_worker.js"
  11. import { maybeUpdateLineNumberWidth } from "./line_numbers.js"
  12. import { measureForScrollbars, updateScrollbars } from "./scrollbars.js"
  13. import { updateSelection } from "./selection.js"
  14. import { updateHeightsInViewport, visibleLines } from "./update_lines.js"
  15. import { adjustView, countDirtyView, resetView } from "./view_tracking.js"
  16. // DISPLAY DRAWING
  17. export class DisplayUpdate {
  18. constructor(cm, viewport, force) {
  19. let display = cm.display
  20. this.viewport = viewport
  21. // Store some values that we'll need later (but don't want to force a relayout for)
  22. this.visible = visibleLines(display, cm.doc, viewport)
  23. this.editorIsHidden = !display.wrapper.offsetWidth
  24. this.wrapperHeight = display.wrapper.clientHeight
  25. this.wrapperWidth = display.wrapper.clientWidth
  26. this.oldDisplayWidth = displayWidth(cm)
  27. this.force = force
  28. this.dims = getDimensions(cm)
  29. this.events = []
  30. }
  31. signal(emitter, type) {
  32. if (hasHandler(emitter, type))
  33. this.events.push(arguments)
  34. }
  35. finish() {
  36. for (let i = 0; i < this.events.length; i++)
  37. signal.apply(null, this.events[i])
  38. }
  39. }
  40. export function maybeClipScrollbars(cm) {
  41. let display = cm.display
  42. if (!display.scrollbarsClipped && display.scroller.offsetWidth) {
  43. display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth
  44. display.heightForcer.style.height = scrollGap(cm) + "px"
  45. display.sizer.style.marginBottom = -display.nativeBarWidth + "px"
  46. display.sizer.style.borderRightWidth = scrollGap(cm) + "px"
  47. display.scrollbarsClipped = true
  48. }
  49. }
  50. function selectionSnapshot(cm) {
  51. if (cm.hasFocus()) return null
  52. let active = activeElt()
  53. if (!active || !contains(cm.display.lineDiv, active)) return null
  54. let result = {activeElt: active}
  55. if (window.getSelection) {
  56. let sel = window.getSelection()
  57. if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) {
  58. result.anchorNode = sel.anchorNode
  59. result.anchorOffset = sel.anchorOffset
  60. result.focusNode = sel.focusNode
  61. result.focusOffset = sel.focusOffset
  62. }
  63. }
  64. return result
  65. }
  66. function restoreSelection(snapshot) {
  67. if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) return
  68. snapshot.activeElt.focus()
  69. if (snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) {
  70. let sel = window.getSelection(), range = document.createRange()
  71. range.setEnd(snapshot.anchorNode, snapshot.anchorOffset)
  72. range.collapse(false)
  73. sel.removeAllRanges()
  74. sel.addRange(range)
  75. sel.extend(snapshot.focusNode, snapshot.focusOffset)
  76. }
  77. }
  78. // Does the actual updating of the line display. Bails out
  79. // (returning false) when there is nothing to be done and forced is
  80. // false.
  81. export function updateDisplayIfNeeded(cm, update) {
  82. let display = cm.display, doc = cm.doc
  83. if (update.editorIsHidden) {
  84. resetView(cm)
  85. return false
  86. }
  87. // Bail out if the visible area is already rendered and nothing changed.
  88. if (!update.force &&
  89. update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&
  90. (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&
  91. display.renderedView == display.view && countDirtyView(cm) == 0)
  92. return false
  93. if (maybeUpdateLineNumberWidth(cm)) {
  94. resetView(cm)
  95. update.dims = getDimensions(cm)
  96. }
  97. // Compute a suitable new viewport (from & to)
  98. let end = doc.first + doc.size
  99. let from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first)
  100. let to = Math.min(end, update.visible.to + cm.options.viewportMargin)
  101. if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom)
  102. if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo)
  103. if (sawCollapsedSpans) {
  104. from = visualLineNo(cm.doc, from)
  105. to = visualLineEndNo(cm.doc, to)
  106. }
  107. let different = from != display.viewFrom || to != display.viewTo ||
  108. display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth
  109. adjustView(cm, from, to)
  110. display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom))
  111. // Position the mover div to align with the current scroll position
  112. cm.display.mover.style.top = display.viewOffset + "px"
  113. let toUpdate = countDirtyView(cm)
  114. if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view &&
  115. (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))
  116. return false
  117. // For big changes, we hide the enclosing element during the
  118. // update, since that speeds up the operations on most browsers.
  119. let selSnapshot = selectionSnapshot(cm)
  120. if (toUpdate > 4) display.lineDiv.style.display = "none"
  121. patchDisplay(cm, display.updateLineNumbers, update.dims)
  122. if (toUpdate > 4) display.lineDiv.style.display = ""
  123. display.renderedView = display.view
  124. // There might have been a widget with a focused element that got
  125. // hidden or updated, if so re-focus it.
  126. restoreSelection(selSnapshot)
  127. // Prevent selection and cursors from interfering with the scroll
  128. // width and height.
  129. removeChildren(display.cursorDiv)
  130. removeChildren(display.selectionDiv)
  131. display.gutters.style.height = display.sizer.style.minHeight = 0
  132. if (different) {
  133. display.lastWrapHeight = update.wrapperHeight
  134. display.lastWrapWidth = update.wrapperWidth
  135. startWorker(cm, 400)
  136. }
  137. display.updateLineNumbers = null
  138. return true
  139. }
  140. export function postUpdateDisplay(cm, update) {
  141. let viewport = update.viewport
  142. for (let first = true;; first = false) {
  143. if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) {
  144. // Clip forced viewport to actual scrollable area.
  145. if (viewport && viewport.top != null)
  146. viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}
  147. // Updated line heights might result in the drawn area not
  148. // actually covering the viewport. Keep looping until it does.
  149. update.visible = visibleLines(cm.display, cm.doc, viewport)
  150. if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
  151. break
  152. }
  153. if (!updateDisplayIfNeeded(cm, update)) break
  154. updateHeightsInViewport(cm)
  155. let barMeasure = measureForScrollbars(cm)
  156. updateSelection(cm)
  157. updateScrollbars(cm, barMeasure)
  158. setDocumentHeight(cm, barMeasure)
  159. update.force = false
  160. }
  161. update.signal(cm, "update", cm)
  162. if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) {
  163. update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo)
  164. cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo
  165. }
  166. }
  167. export function updateDisplaySimple(cm, viewport) {
  168. let update = new DisplayUpdate(cm, viewport)
  169. if (updateDisplayIfNeeded(cm, update)) {
  170. updateHeightsInViewport(cm)
  171. postUpdateDisplay(cm, update)
  172. let barMeasure = measureForScrollbars(cm)
  173. updateSelection(cm)
  174. updateScrollbars(cm, barMeasure)
  175. setDocumentHeight(cm, barMeasure)
  176. update.finish()
  177. }
  178. }
  179. // Sync the actual display DOM structure with display.view, removing
  180. // nodes for lines that are no longer in view, and creating the ones
  181. // that are not there yet, and updating the ones that are out of
  182. // date.
  183. function patchDisplay(cm, updateNumbersFrom, dims) {
  184. let display = cm.display, lineNumbers = cm.options.lineNumbers
  185. let container = display.lineDiv, cur = container.firstChild
  186. function rm(node) {
  187. let next = node.nextSibling
  188. // Works around a throw-scroll bug in OS X Webkit
  189. if (webkit && mac && cm.display.currentWheelTarget == node)
  190. node.style.display = "none"
  191. else
  192. node.parentNode.removeChild(node)
  193. return next
  194. }
  195. let view = display.view, lineN = display.viewFrom
  196. // Loop over the elements in the view, syncing cur (the DOM nodes
  197. // in display.lineDiv) with the view as we go.
  198. for (let i = 0; i < view.length; i++) {
  199. let lineView = view[i]
  200. if (lineView.hidden) {
  201. } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet
  202. let node = buildLineElement(cm, lineView, lineN, dims)
  203. container.insertBefore(node, cur)
  204. } else { // Already drawn
  205. while (cur != lineView.node) cur = rm(cur)
  206. let updateNumber = lineNumbers && updateNumbersFrom != null &&
  207. updateNumbersFrom <= lineN && lineView.lineNumber
  208. if (lineView.changes) {
  209. if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false
  210. updateLineForChanges(cm, lineView, lineN, dims)
  211. }
  212. if (updateNumber) {
  213. removeChildren(lineView.lineNumber)
  214. lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN)))
  215. }
  216. cur = lineView.node.nextSibling
  217. }
  218. lineN += lineView.size
  219. }
  220. while (cur) cur = rm(cur)
  221. }
  222. export function updateGutterSpace(cm) {
  223. let width = cm.display.gutters.offsetWidth
  224. cm.display.sizer.style.marginLeft = width + "px"
  225. }
  226. export function setDocumentHeight(cm, measure) {
  227. cm.display.sizer.style.minHeight = measure.docHeight + "px"
  228. cm.display.heightForcer.style.top = measure.docHeight + "px"
  229. cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px"
  230. }