26ddbc9cdc00819a4ca3d3efe7132458b9003aee93155c66cf97336115b3cbae0d13053d99eaf66f3b59916b1364beb9e84c881ad4e4f580f1b98a4909c0a7 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import { clipPos } from "../line/pos.js"
  2. import { findMaxLine } from "../line/spans.js"
  3. import { displayWidth, measureChar, scrollGap } from "../measurement/position_measurement.js"
  4. import { signal } from "../util/event.js"
  5. import { activeElt, root } from "../util/dom.js"
  6. import { finishOperation, pushOperation } from "../util/operation_group.js"
  7. import { ensureFocus } from "./focus.js"
  8. import { measureForScrollbars, updateScrollbars } from "./scrollbars.js"
  9. import { restartBlink } from "./selection.js"
  10. import { maybeScrollWindow, scrollPosIntoView, setScrollLeft, setScrollTop } from "./scrolling.js"
  11. import { DisplayUpdate, maybeClipScrollbars, postUpdateDisplay, setDocumentHeight, updateDisplayIfNeeded } from "./update_display.js"
  12. import { updateHeightsInViewport } from "./update_lines.js"
  13. // Operations are used to wrap a series of changes to the editor
  14. // state in such a way that each change won't have to update the
  15. // cursor and display (which would be awkward, slow, and
  16. // error-prone). Instead, display updates are batched and then all
  17. // combined and executed at once.
  18. let nextOpId = 0
  19. // Start a new operation.
  20. export function startOperation(cm) {
  21. cm.curOp = {
  22. cm: cm,
  23. viewChanged: false, // Flag that indicates that lines might need to be redrawn
  24. startHeight: cm.doc.height, // Used to detect need to update scrollbar
  25. forceUpdate: false, // Used to force a redraw
  26. updateInput: 0, // Whether to reset the input textarea
  27. typing: false, // Whether this reset should be careful to leave existing text (for compositing)
  28. changeObjs: null, // Accumulated changes, for firing change events
  29. cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on
  30. cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already
  31. selectionChanged: false, // Whether the selection needs to be redrawn
  32. updateMaxLine: false, // Set when the widest line needs to be determined anew
  33. scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet
  34. scrollToPos: null, // Used to scroll to a specific position
  35. focus: false,
  36. id: ++nextOpId, // Unique ID
  37. markArrays: null // Used by addMarkedSpan
  38. }
  39. pushOperation(cm.curOp)
  40. }
  41. // Finish an operation, updating the display and signalling delayed events
  42. export function endOperation(cm) {
  43. let op = cm.curOp
  44. if (op) finishOperation(op, group => {
  45. for (let i = 0; i < group.ops.length; i++)
  46. group.ops[i].cm.curOp = null
  47. endOperations(group)
  48. })
  49. }
  50. // The DOM updates done when an operation finishes are batched so
  51. // that the minimum number of relayouts are required.
  52. function endOperations(group) {
  53. let ops = group.ops
  54. for (let i = 0; i < ops.length; i++) // Read DOM
  55. endOperation_R1(ops[i])
  56. for (let i = 0; i < ops.length; i++) // Write DOM (maybe)
  57. endOperation_W1(ops[i])
  58. for (let i = 0; i < ops.length; i++) // Read DOM
  59. endOperation_R2(ops[i])
  60. for (let i = 0; i < ops.length; i++) // Write DOM (maybe)
  61. endOperation_W2(ops[i])
  62. for (let i = 0; i < ops.length; i++) // Read DOM
  63. endOperation_finish(ops[i])
  64. }
  65. function endOperation_R1(op) {
  66. let cm = op.cm, display = cm.display
  67. maybeClipScrollbars(cm)
  68. if (op.updateMaxLine) findMaxLine(cm)
  69. op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null ||
  70. op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||
  71. op.scrollToPos.to.line >= display.viewTo) ||
  72. display.maxLineChanged && cm.options.lineWrapping
  73. op.update = op.mustUpdate &&
  74. new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate)
  75. }
  76. function endOperation_W1(op) {
  77. op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update)
  78. }
  79. function endOperation_R2(op) {
  80. let cm = op.cm, display = cm.display
  81. if (op.updatedDisplay) updateHeightsInViewport(cm)
  82. op.barMeasure = measureForScrollbars(cm)
  83. // If the max line changed since it was last measured, measure it,
  84. // and ensure the document's width matches it.
  85. // updateDisplay_W2 will use these properties to do the actual resizing
  86. if (display.maxLineChanged && !cm.options.lineWrapping) {
  87. op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3
  88. cm.display.sizerWidth = op.adjustWidthTo
  89. op.barMeasure.scrollWidth =
  90. Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth)
  91. op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm))
  92. }
  93. if (op.updatedDisplay || op.selectionChanged)
  94. op.preparedSelection = display.input.prepareSelection()
  95. }
  96. function endOperation_W2(op) {
  97. let cm = op.cm
  98. if (op.adjustWidthTo != null) {
  99. cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"
  100. if (op.maxScrollLeft < cm.doc.scrollLeft)
  101. setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true)
  102. cm.display.maxLineChanged = false
  103. }
  104. let takeFocus = op.focus && op.focus == activeElt(root(cm))
  105. if (op.preparedSelection)
  106. cm.display.input.showSelection(op.preparedSelection, takeFocus)
  107. if (op.updatedDisplay || op.startHeight != cm.doc.height)
  108. updateScrollbars(cm, op.barMeasure)
  109. if (op.updatedDisplay)
  110. setDocumentHeight(cm, op.barMeasure)
  111. if (op.selectionChanged) restartBlink(cm)
  112. if (cm.state.focused && op.updateInput)
  113. cm.display.input.reset(op.typing)
  114. if (takeFocus) ensureFocus(op.cm)
  115. }
  116. function endOperation_finish(op) {
  117. let cm = op.cm, display = cm.display, doc = cm.doc
  118. if (op.updatedDisplay) postUpdateDisplay(cm, op.update)
  119. // Abort mouse wheel delta measurement, when scrolling explicitly
  120. if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
  121. display.wheelStartX = display.wheelStartY = null
  122. // Propagate the scroll position to the actual DOM scroller
  123. if (op.scrollTop != null) setScrollTop(cm, op.scrollTop, op.forceScroll)
  124. if (op.scrollLeft != null) setScrollLeft(cm, op.scrollLeft, true, true)
  125. // If we need to scroll a specific position into view, do so.
  126. if (op.scrollToPos) {
  127. let rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),
  128. clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin)
  129. maybeScrollWindow(cm, rect)
  130. }
  131. // Fire events for markers that are hidden/unidden by editing or
  132. // undoing
  133. let hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers
  134. if (hidden) for (let i = 0; i < hidden.length; ++i)
  135. if (!hidden[i].lines.length) signal(hidden[i], "hide")
  136. if (unhidden) for (let i = 0; i < unhidden.length; ++i)
  137. if (unhidden[i].lines.length) signal(unhidden[i], "unhide")
  138. if (display.wrapper.offsetHeight)
  139. doc.scrollTop = cm.display.scroller.scrollTop
  140. // Fire change events, and delayed event handlers
  141. if (op.changeObjs)
  142. signal(cm, "changes", cm, op.changeObjs)
  143. if (op.update)
  144. op.update.finish()
  145. }
  146. // Run the given function in an operation
  147. export function runInOp(cm, f) {
  148. if (cm.curOp) return f()
  149. startOperation(cm)
  150. try { return f() }
  151. finally { endOperation(cm) }
  152. }
  153. // Wraps a function in an operation. Returns the wrapped function.
  154. export function operation(cm, f) {
  155. return function() {
  156. if (cm.curOp) return f.apply(cm, arguments)
  157. startOperation(cm)
  158. try { return f.apply(cm, arguments) }
  159. finally { endOperation(cm) }
  160. }
  161. }
  162. // Used to add methods to editor and doc instances, wrapping them in
  163. // operations.
  164. export function methodOp(f) {
  165. return function() {
  166. if (this.curOp) return f.apply(this, arguments)
  167. startOperation(this)
  168. try { return f.apply(this, arguments) }
  169. finally { endOperation(this) }
  170. }
  171. }
  172. export function docMethodOp(f) {
  173. return function() {
  174. let cm = this.cm
  175. if (!cm || cm.curOp) return f.apply(this, arguments)
  176. startOperation(cm)
  177. try { return f.apply(this, arguments) }
  178. finally { endOperation(cm) }
  179. }
  180. }