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