import { clipPos } from "../line/pos.js" import { findMaxLine } from "../line/spans.js" import { displayWidth, measureChar, scrollGap } from "../measurement/position_measurement.js" import { signal } from "../util/event.js" import { activeElt } from "../util/dom.js" import { finishOperation, pushOperation } from "../util/operation_group.js" import { ensureFocus } from "./focus.js" import { measureForScrollbars, updateScrollbars } from "./scrollbars.js" import { restartBlink } from "./selection.js" import { maybeScrollWindow, scrollPosIntoView, setScrollLeft, setScrollTop } from "./scrolling.js" import { DisplayUpdate, maybeClipScrollbars, postUpdateDisplay, setDocumentHeight, updateDisplayIfNeeded } from "./update_display.js" import { updateHeightsInViewport } from "./update_lines.js" // Operations are used to wrap a series of changes to the editor // state in such a way that each change won't have to update the // cursor and display (which would be awkward, slow, and // error-prone). Instead, display updates are batched and then all // combined and executed at once. let nextOpId = 0 // Start a new operation. export function startOperation(cm) { cm.curOp = { cm: cm, viewChanged: false, // Flag that indicates that lines might need to be redrawn startHeight: cm.doc.height, // Used to detect need to update scrollbar forceUpdate: false, // Used to force a redraw updateInput: 0, // Whether to reset the input textarea typing: false, // Whether this reset should be careful to leave existing text (for compositing) changeObjs: null, // Accumulated changes, for firing change events cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already selectionChanged: false, // Whether the selection needs to be redrawn updateMaxLine: false, // Set when the widest line needs to be determined anew scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet scrollToPos: null, // Used to scroll to a specific position focus: false, id: ++nextOpId // Unique ID } pushOperation(cm.curOp) } // Finish an operation, updating the display and signalling delayed events export function endOperation(cm) { let op = cm.curOp if (op) finishOperation(op, group => { for (let i = 0; i < group.ops.length; i++) group.ops[i].cm.curOp = null endOperations(group) }) } // The DOM updates done when an operation finishes are batched so // that the minimum number of relayouts are required. function endOperations(group) { let ops = group.ops for (let i = 0; i < ops.length; i++) // Read DOM endOperation_R1(ops[i]) for (let i = 0; i < ops.length; i++) // Write DOM (maybe) endOperation_W1(ops[i]) for (let i = 0; i < ops.length; i++) // Read DOM endOperation_R2(ops[i]) for (let i = 0; i < ops.length; i++) // Write DOM (maybe) endOperation_W2(ops[i]) for (let i = 0; i < ops.length; i++) // Read DOM endOperation_finish(ops[i]) } function endOperation_R1(op) { let cm = op.cm, display = cm.display maybeClipScrollbars(cm) if (op.updateMaxLine) findMaxLine(cm) op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || op.scrollToPos.to.line >= display.viewTo) || display.maxLineChanged && cm.options.lineWrapping op.update = op.mustUpdate && new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate) } function endOperation_W1(op) { op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update) } function endOperation_R2(op) { let cm = op.cm, display = cm.display if (op.updatedDisplay) updateHeightsInViewport(cm) op.barMeasure = measureForScrollbars(cm) // If the max line changed since it was last measured, measure it, // and ensure the document's width matches it. // updateDisplay_W2 will use these properties to do the actual resizing if (display.maxLineChanged && !cm.options.lineWrapping) { op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3 cm.display.sizerWidth = op.adjustWidthTo op.barMeasure.scrollWidth = Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth) op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)) } if (op.updatedDisplay || op.selectionChanged) op.preparedSelection = display.input.prepareSelection() } function endOperation_W2(op) { let cm = op.cm if (op.adjustWidthTo != null) { cm.display.sizer.style.minWidth = op.adjustWidthTo + "px" if (op.maxScrollLeft < cm.doc.scrollLeft) setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true) cm.display.maxLineChanged = false } let takeFocus = op.focus && op.focus == activeElt() if (op.preparedSelection) cm.display.input.showSelection(op.preparedSelection, takeFocus) if (op.updatedDisplay || op.startHeight != cm.doc.height) updateScrollbars(cm, op.barMeasure) if (op.updatedDisplay) setDocumentHeight(cm, op.barMeasure) if (op.selectionChanged) restartBlink(cm) if (cm.state.focused && op.updateInput) cm.display.input.reset(op.typing) if (takeFocus) ensureFocus(op.cm) } function endOperation_finish(op) { let cm = op.cm, display = cm.display, doc = cm.doc if (op.updatedDisplay) postUpdateDisplay(cm, op.update) // Abort mouse wheel delta measurement, when scrolling explicitly if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) display.wheelStartX = display.wheelStartY = null // Propagate the scroll position to the actual DOM scroller if (op.scrollTop != null) setScrollTop(cm, op.scrollTop, op.forceScroll) if (op.scrollLeft != null) setScrollLeft(cm, op.scrollLeft, true, true) // If we need to scroll a specific position into view, do so. if (op.scrollToPos) { let rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin) maybeScrollWindow(cm, rect) } // Fire events for markers that are hidden/unidden by editing or // undoing let hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers if (hidden) for (let i = 0; i < hidden.length; ++i) if (!hidden[i].lines.length) signal(hidden[i], "hide") if (unhidden) for (let i = 0; i < unhidden.length; ++i) if (unhidden[i].lines.length) signal(unhidden[i], "unhide") if (display.wrapper.offsetHeight) doc.scrollTop = cm.display.scroller.scrollTop // Fire change events, and delayed event handlers if (op.changeObjs) signal(cm, "changes", cm, op.changeObjs) if (op.update) op.update.finish() } // Run the given function in an operation export function runInOp(cm, f) { if (cm.curOp) return f() startOperation(cm) try { return f() } finally { endOperation(cm) } } // Wraps a function in an operation. Returns the wrapped function. export function operation(cm, f) { return function() { if (cm.curOp) return f.apply(cm, arguments) startOperation(cm) try { return f.apply(cm, arguments) } finally { endOperation(cm) } } } // Used to add methods to editor and doc instances, wrapping them in // operations. export function methodOp(f) { return function() { if (this.curOp) return f.apply(this, arguments) startOperation(this) try { return f.apply(this, arguments) } finally { endOperation(this) } } } export function docMethodOp(f) { return function() { let cm = this.cm if (!cm || cm.curOp) return f.apply(this, arguments) startOperation(cm) try { return f.apply(this, arguments) } finally { endOperation(cm) } } }