diff --git a/lib/codemirror.js b/lib/codemirror.js index b2e194273f22d0acc5401b7d05780703e3b6ff6a..1bf02d2089fa0351b7581e00fd43803e88b562f3 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -108,6 +108,7 @@ for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) optionHandlers[opt](cm, options[opt], Init); + maybeUpdateLineNumberWidth(cm); for (var i = 0; i < initHooks.length; ++i) initHooks[i](cm); }); } @@ -467,18 +468,18 @@ } // Compute the lines that are visible in a given viewport (defaults - // the the current scroll position). viewPort may contain top, + // the the current scroll position). viewport may contain top, // height, and ensure (see op.scrollToPos) properties. - function visibleLines(display, doc, viewPort) { - var top = viewPort && viewPort.top != null ? Math.max(0, viewPort.top) : display.scroller.scrollTop; + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; top = Math.floor(top - paddingTop(display)); - var bottom = viewPort && viewPort.bottom != null ? viewPort.bottom : top + display.wrapper.clientHeight; + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); // Ensure is a {from: {line, ch}, to: {line, ch}} object, and // forces those lines into the viewport (if possible). - if (viewPort && viewPort.ensure) { - var ensureFrom = viewPort.ensure.from.line, ensureTo = viewPort.ensure.to.line; + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; if (ensureFrom < from) return {from: ensureFrom, to: lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight)}; @@ -543,83 +544,46 @@ // DISPLAY DRAWING - // Updates the display, selection, and scrollbars, using the - // information in display.view to find out which nodes are no longer - // up-to-date. Tries to bail out early when no changes are needed, - // unless forced is true. - // Returns true if an actual update happened, false otherwise. - function updateDisplay(cm, viewPort, forced) { - var oldFrom = cm.display.viewFrom, oldTo = cm.display.viewTo, updated; - var visible = visibleLines(cm.display, cm.doc, viewPort); - for (var first = true;; first = false) { - var oldWidth = cm.display.scroller.clientWidth; - if (!updateDisplayInner(cm, visible, forced)) break; - updated = true; - - // If the max line changed since it was last measured, measure it, - // and ensure the document's width matches it. - if (cm.display.maxLineChanged && !cm.options.lineWrapping) - adjustContentWidth(cm); - - var barMeasure = measureForScrollbars(cm); - updateSelection(cm); - setDocumentHeight(cm, barMeasure); - updateScrollbars(cm, barMeasure); - if (webkit && cm.options.lineWrapping) - checkForWebkitWidthBug(cm, barMeasure); // (Issue #2420) - if (webkit && barMeasure.scrollWidth > barMeasure.clientWidth && - barMeasure.scrollWidth < barMeasure.clientWidth + 1 && - !hScrollbarTakesSpace(cm)) - updateScrollbars(cm); // (Issue #2562) - if (first && cm.options.lineWrapping && oldWidth != cm.display.scroller.clientWidth) { - forced = true; - continue; - } - forced = false; - - // Clip forced viewport to actual scrollable area. - if (viewPort && viewPort.top != null) - viewPort = {top: Math.min(barMeasure.docHeight - scrollerCutOff - barMeasure.clientHeight, viewPort.top)}; - // Updated line heights might result in the drawn area not - // actually covering the viewport. Keep looping until it does. - visible = visibleLines(cm.display, cm.doc, viewPort); - if (visible.from >= cm.display.viewFrom && visible.to <= cm.display.viewTo) - break; - } + function DisplayUpdate(cm, viewport, force) { + var display = cm.display; - cm.display.updateLineNumbers = null; - if (updated) { - signalLater(cm, "update", cm); - if (cm.display.viewFrom != oldFrom || cm.display.viewTo != oldTo) - signalLater(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); - } - return updated; + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.oldViewFrom = display.viewFrom; this.oldViewTo = display.viewTo; + this.oldScrollerWidth = display.scroller.clientWidth; + this.force = force; + this.dims = getDimensions(cm); } // Does the actual updating of the line display. Bails out // (returning false) when there is nothing to be done and forced is // false. - function updateDisplayInner(cm, visible, forced) { + function updateDisplayIfNeeded(cm, update) { var display = cm.display, doc = cm.doc; - if (!display.wrapper.offsetWidth) { + if (update.editorIsHidden) { resetView(cm); - return; + return false; } // Bail out if the visible area is already rendered and nothing changed. - if (!forced && visible.from >= display.viewFrom && visible.to <= display.viewTo && + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && countDirtyView(cm) == 0) - return; + return false; - if (maybeUpdateLineNumberWidth(cm)) + if (maybeUpdateLineNumberWidth(cm)) { resetView(cm); - var dims = getDimensions(cm); + update.dims = getDimensions(cm); + } // Compute a suitable new viewport (from & to) var end = doc.first + doc.size; - var from = Math.max(visible.from - cm.options.viewportMargin, doc.first); - var to = Math.min(end, visible.to + cm.options.viewportMargin); + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); if (sawCollapsedSpans) { @@ -628,7 +592,7 @@ } var different = from != display.viewFrom || to != display.viewTo || - display.lastSizeC != display.wrapper.clientHeight; + display.lastSizeC != update.wrapperHeight; adjustView(cm, from, to); display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); @@ -636,13 +600,15 @@ cm.display.mover.style.top = display.viewOffset + "px"; var toUpdate = countDirtyView(cm); - if (!different && toUpdate == 0 && !forced) return; + if (!different && toUpdate == 0 && !update.force && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + return false; // For big changes, we hide the enclosing element during the // update, since that speeds up the operations on most browsers. var focused = activeElt(); if (toUpdate > 4) display.lineDiv.style.display = "none"; - patchDisplay(cm, display.updateLineNumbers, dims); + patchDisplay(cm, display.updateLineNumbers, update.dims); if (toUpdate > 4) display.lineDiv.style.display = ""; // There might have been a widget with a focused element that got // hidden or updated, if so re-focus it. @@ -654,24 +620,50 @@ removeChildren(display.selectionDiv); if (different) { - display.lastSizeC = display.wrapper.clientHeight; + display.lastSizeC = update.wrapperHeight; startWorker(cm, 400); } - updateHeightsInViewport(cm); + display.updateLineNumbers = null; return true; } - function adjustContentWidth(cm) { - var display = cm.display; - var width = measureChar(cm, display.maxLine, display.maxLine.text.length).left; - display.maxLineChanged = false; - var minWidth = Math.max(0, width + 3); - var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + minWidth + scrollerCutOff - display.scroller.clientWidth); - display.sizer.style.minWidth = minWidth + "px"; - if (maxScrollLeft < cm.doc.scrollLeft) - setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true); + function postUpdateDisplay(cm, update) { + var force = update.force, viewport = update.viewport; + for (var first = true;; first = false) { + updateHeightsInViewport(cm); + if (first && cm.options.lineWrapping && update.oldScrollerWidth != cm.display.scroller.clientWidth) { + force = true; + } else { + force = false; + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - scrollerCutOff - + cm.display.scroller.clientHeight, viewport.top)}; + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + var visible = visibleLines(cm.display, cm.doc, viewport); + if (visible.from >= cm.display.viewFrom && visible.to <= cm.display.viewTo) + break; + } + if (!updateDisplayIfNeeded(cm, update)) break; + } + + signalLater(cm, "update", cm); + if (cm.display.viewFrom != update.oldViewFrom || cm.display.viewTo != update.oldViewTo) + signalLater(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + } } function setDocumentHeight(cm, measure) { @@ -1257,10 +1249,10 @@ // SELECTION DRAWING // Redraw the selection and/or cursor - function updateSelection(cm) { - var display = cm.display, doc = cm.doc; - var curFragment = document.createDocumentFragment(); - var selFragment = document.createDocumentFragment(); + function drawSelection(cm) { + var display = cm.display, doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); for (var i = 0; i < doc.sel.ranges.length; i++) { var range = doc.sel.ranges[i]; @@ -1275,16 +1267,23 @@ if (cm.options.moveInputWithCursor) { var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); - var top = Math.max(0, Math.min(display.wrapper.clientHeight - 10, - headPos.top + lineOff.top - wrapOff.top)); - var left = Math.max(0, Math.min(display.wrapper.clientWidth - 10, - headPos.left + lineOff.left - wrapOff.left)); - display.inputDiv.style.top = top + "px"; - display.inputDiv.style.left = left + "px"; + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); } - removeChildrenAndAdd(display.cursorDiv, curFragment); - removeChildrenAndAdd(display.selectionDiv, selFragment); + return result; + } + + function updateSelection(cm, drawn) { + if (!drawn) drawn = drawSelection(cm); + removeChildrenAndAdd(cm.display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(cm.display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + cm.display.inputDiv.style.top = drawn.teTop + "px"; + cm.display.inputDiv.style.left = drawn.teLeft + "px"; + } } // Draws a cursor for the given range @@ -1967,28 +1966,88 @@ if (!group) return; try { fireCallbacksForOps(group); } - finally { - cm.curOp = operationGroup = null; + finally { + operationGroup = null; for (var i = 0; i < group.ops.length; i++) - endOperationInner(group.ops[i]); + group.ops[i].cm.curOp = null; + endOperations(group); } } - function endOperationInner(op) { - var cm = op.cm, display = cm.display, doc = cm.doc; + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R1(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W1(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R2(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W2(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_finish(ops[i]); + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; if (op.updateMaxLine) findMaxLine(cm); - // If it looks like an update might be needed, call updateDisplay - if (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) { - var updated = updateDisplay(cm, {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); - if (cm.display.scroller.offsetHeight) doc.scrollTop = cm.display.scroller.scrollTop; + 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) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) postUpdateDisplay(cm, op.update); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplayIfNeeded 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; + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo + + scrollerCutOff - display.scroller.clientWidth); } - // If no update was run, but the selection changed, redraw that. - if (!updated && op.selectionChanged) updateSelection(cm); - if (!updated && op.startHeight != doc.height) updateScrollbars(cm); + + op.barMeasure = measureForScrollbars(cm); + if (op.updatedDisplay || op.selectionChanged) + op.newSelectionNodes = drawSelection(cm); + } + + function endOperation_W2(op) { + var 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); + } + + if (op.newSelectionNodes) + updateSelection(cm, op.newSelectionNodes); + if (op.updatedDisplay) + setDocumentHeight(cm, op.barMeasure); + if (op.updatedDisplay || op.startHeight != cm.doc.height) + updateScrollbars(cm, op.barMeasure); + + if (op.selectionChanged) restartBlink(cm); + + if (cm.state.focused && op.updateInput) + resetInput(cm, op.typing); + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; // Abort mouse wheel delta measurement, when scrolling explicitly if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) @@ -2011,11 +2070,6 @@ if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); } - if (op.selectionChanged) restartBlink(cm); - - if (cm.state.focused && op.updateInput) - resetInput(cm, op.typing); - // Fire events for markers that are hidden/unidden by editing or // undoing var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; @@ -2024,6 +2078,19 @@ if (unhidden) for (var 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; + + // Apply workaround for two webkit bugs + if (op.updatedDisplay && webkit) { + if (cm.options.lineWrapping) + checkForWebkitWidthBug(cm, op.barMeasure); // (Issue #2420) + if (op.barMeasure.scrollWidth > op.barMeasure.clientWidth && + op.barMeasure.scrollWidth < op.barMeasure.clientWidth + 1 && + !hScrollbarTakesSpace(cm)) + updateScrollbars(cm); // (Issue #2562) + } + // Fire change events, and delayed event handlers if (op.changeObjs) signal(cm, "changes", cm, op.changeObjs); @@ -2916,10 +2983,10 @@ function setScrollTop(cm, val) { if (Math.abs(cm.doc.scrollTop - val) < 2) return; cm.doc.scrollTop = val; - if (!gecko) updateDisplay(cm, {top: val}); + if (!gecko) updateDisplaySimple(cm, {top: val}); if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val; - if (gecko) updateDisplay(cm); + if (gecko) updateDisplaySimple(cm); startWorker(cm, 100); } // Sync scroller and scrollbar, ensure the gutter elements are @@ -3002,7 +3069,7 @@ var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; if (pixels < 0) top = Math.max(0, top + pixels - 50); else bot = Math.min(cm.doc.height, bot + pixels + 50); - updateDisplay(cm, {top: top, bottom: bot}); + updateDisplaySimple(cm, {top: top, bottom: bot}); } if (wheelSamples < 20) {