UNPKG

codemirror

Version:

In-browser code editing made bearable

1,176 lines (1,062 loc) 243 kB
// CodeMirror is the only global var we claim window.CodeMirror = (function() { "use strict"; // BROWSER SNIFFING // Crude, but necessary to handle a number of hard-to-feature-detect // bugs and behavior differences. var gecko = /gecko\/\d/i.test(navigator.userAgent); // IE11 currently doesn't count as 'ie', since it has almost none of // the same bugs as earlier versions. Use ie_gt10 to handle // incompatibilities in that version. var old_ie = /MSIE \d/.test(navigator.userAgent); var ie_lt8 = old_ie && (document.documentMode == null || document.documentMode < 8); var ie_lt9 = old_ie && (document.documentMode == null || document.documentMode < 9); var ie_lt10 = old_ie && (document.documentMode == null || document.documentMode < 10); var ie_gt10 = /Trident\/([7-9]|\d{2,})\./.test(navigator.userAgent); var ie = old_ie || ie_gt10; var webkit = /WebKit\//.test(navigator.userAgent); var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); var chrome = /Chrome\//.test(navigator.userAgent); var opera = /Opera\//.test(navigator.userAgent); var safari = /Apple Computer/.test(navigator.vendor); var khtml = /KHTML\//.test(navigator.userAgent); var mac_geLion = /Mac OS X 1\d\D([7-9]|\d\d)\D/.test(navigator.userAgent); var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); var phantom = /PhantomJS/.test(navigator.userAgent); var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); // This is woefully incomplete. Suggestions for alternative methods welcome. var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); var mac = ios || /Mac/.test(navigator.platform); var windows = /win/i.test(navigator.platform); var opera_version = opera && navigator.userAgent.match(/Version\/(\d*\.\d*)/); if (opera_version) opera_version = Number(opera_version[1]); if (opera_version && opera_version >= 15) { opera = false; webkit = true; } // Some browsers use the wrong event properties to signal cmd/ctrl on OS X var flipCtrlCmd = mac && (qtwebkit || opera && (opera_version == null || opera_version < 12.11)); var captureMiddleClick = gecko || (ie && !ie_lt9); // Optimize some code when these features are not used var sawReadOnlySpans = false, sawCollapsedSpans = false; // CONSTRUCTOR function CodeMirror(place, options) { if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); this.options = options = options || {}; // Determine effective options based on given values and defaults. for (var opt in defaults) if (!options.hasOwnProperty(opt) && defaults.hasOwnProperty(opt)) options[opt] = defaults[opt]; setGuttersForLineNumbers(options); var docStart = typeof options.value == "string" ? 0 : options.value.first; var display = this.display = makeDisplay(place, docStart); display.wrapper.CodeMirror = this; updateGutters(this); if (options.autofocus && !mobile) focusInput(this); this.state = {keyMaps: [], overlays: [], modeGen: 0, overwrite: false, focused: false, suppressEdits: false, pasteIncoming: false, cutIncoming: false, draggingText: false, highlight: new Delayed()}; themeChanged(this); if (options.lineWrapping) this.display.wrapper.className += " CodeMirror-wrap"; var doc = options.value; if (typeof doc == "string") doc = new Doc(options.value, options.mode); operation(this, attachDoc)(this, doc); // Override magic textarea content restore that IE sometimes does // on our hidden textarea on reload if (old_ie) setTimeout(bind(resetInput, this, true), 20); registerEventHandlers(this); // IE throws unspecified error in certain cases, when // trying to access activeElement before onload var hasFocus; try { hasFocus = (document.activeElement == display.input); } catch(e) { } if (hasFocus || (options.autofocus && !mobile)) setTimeout(bind(onFocus, this), 20); else onBlur(this); operation(this, function() { for (var opt in optionHandlers) if (optionHandlers.propertyIsEnumerable(opt)) optionHandlers[opt](this, options[opt], Init); for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); })(); } // DISPLAY CONSTRUCTOR function makeDisplay(place, docStart) { var d = {}; var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); if (webkit) input.style.width = "1000px"; else input.setAttribute("wrap", "off"); // if border: 0; -- iOS fails to open keyboard (issue #1287) if (ios) input.style.border = "1px solid black"; input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); input.setAttribute("spellcheck", "false"); // Wraps and hides input textarea d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); // The actual fake scrollbars. d.scrollbarH = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); d.scrollbarV = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); // DIVs containing the selection and the actual code d.lineDiv = elt("div", null, "CodeMirror-code"); d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); // Blinky cursor, and element used to ensure cursor fits at the end of a line d.cursor = elt("div", "\u00a0", "CodeMirror-cursor"); // Secondary cursor, shown when on a 'jump' in bi-directional text d.otherCursor = elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor"); // Used to measure text size d.measure = elt("div", null, "CodeMirror-measure"); // Wraps everything that needs to exist inside the vertically-padded coordinate system d.lineSpace = elt("div", [d.measure, d.selectionDiv, d.lineDiv, d.cursor, d.otherCursor], null, "position: relative; outline: none"); // Moved around its parent to cover visible view d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); // Set to the height of the text, causes scrolling d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); // D is needed because behavior of elts with overflow: auto and padding is inconsistent across browsers d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerCutOff + "px; width: 1px;"); // Will contain the gutters, if any d.gutters = elt("div", null, "CodeMirror-gutters"); d.lineGutter = null; // Provides scrolling d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); d.scroller.setAttribute("tabIndex", "-1"); // The element in which the editor lives. d.wrapper = elt("div", [d.inputDiv, d.scrollbarH, d.scrollbarV, d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); // Work around IE7 z-index bug if (ie_lt8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } if (place.appendChild) place.appendChild(d.wrapper); else place(d.wrapper); // Needed to hide big blue blinking cursor on Mobile Safari if (ios) input.style.width = "0px"; if (!webkit) d.scroller.draggable = true; // Needed to handle Tab key in KHTML if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; } // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). else if (ie_lt8) d.scrollbarH.style.minWidth = d.scrollbarV.style.minWidth = "18px"; // Current visible range (may be bigger than the view window). d.viewOffset = d.lastSizeC = 0; d.showingFrom = d.showingTo = docStart; // Used to only resize the line number gutter when necessary (when // the amount of lines crosses a boundary that makes its width change) d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; // See readInput and resetInput d.prevInput = ""; // Set to true when a non-horizontal-scrolling widget is added. As // an optimization, widget aligning is skipped when d is false. d.alignWidgets = false; // Flag that indicates whether we currently expect input to appear // (after some event like 'keypress' or 'input') and are polling // intensively. d.pollingFast = false; // Self-resetting timeout for the poller d.poll = new Delayed(); d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; d.measureLineCache = []; d.measureLineCachePos = 0; // Tracks when resetInput has punted to just putting a short // string instead of the (large) selection. d.inaccurateSelection = false; // Tracks the maximum line length so that the horizontal scrollbar // can be kept static when scrolling. d.maxLine = null; d.maxLineLength = 0; d.maxLineChanged = false; // Used for measuring wheel scrolling granularity d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; return d; } // STATE UPDATES // Used to get the editor into a consistent state again when options change. function loadMode(cm) { cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); resetModeState(cm); } function resetModeState(cm) { cm.doc.iter(function(line) { if (line.stateAfter) line.stateAfter = null; if (line.styles) line.styles = null; }); cm.doc.frontier = cm.doc.first; startWorker(cm, 100); cm.state.modeGen++; if (cm.curOp) regChange(cm); } function wrappingChanged(cm) { if (cm.options.lineWrapping) { cm.display.wrapper.className += " CodeMirror-wrap"; cm.display.sizer.style.minWidth = ""; } else { cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-wrap", ""); computeMaxLength(cm); } estimateLineHeights(cm); regChange(cm); clearCaches(cm); setTimeout(function(){updateScrollbars(cm);}, 100); } function estimateHeight(cm) { var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); return function(line) { if (lineIsHidden(cm.doc, line)) return 0; var widgetsHeight = 0; if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; } if (wrapping) return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; else return widgetsHeight + th; }; } function estimateLineHeights(cm) { var doc = cm.doc, est = estimateHeight(cm); doc.iter(function(line) { var estHeight = est(line); if (estHeight != line.height) updateLineHeight(line, estHeight); }); } function keyMapChanged(cm) { var map = keyMap[cm.options.keyMap], style = map.style; cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-keymap-\S+/g, "") + (style ? " cm-keymap-" + style : ""); } function themeChanged(cm) { cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); clearCaches(cm); } function guttersChanged(cm) { updateGutters(cm); regChange(cm); setTimeout(function(){alignHorizontally(cm);}, 20); } function updateGutters(cm) { var gutters = cm.display.gutters, specs = cm.options.gutters; removeChildren(gutters); for (var i = 0; i < specs.length; ++i) { var gutterClass = specs[i]; var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); if (gutterClass == "CodeMirror-linenumbers") { cm.display.lineGutter = gElt; gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; } } gutters.style.display = i ? "" : "none"; } function lineLength(doc, line) { if (line.height == 0) return 0; var len = line.text.length, merged, cur = line; while (merged = collapsedSpanAtStart(cur)) { var found = merged.find(); cur = getLine(doc, found.from.line); len += found.from.ch - found.to.ch; } cur = line; while (merged = collapsedSpanAtEnd(cur)) { var found = merged.find(); len -= cur.text.length - found.from.ch; cur = getLine(doc, found.to.line); len += cur.text.length - found.to.ch; } return len; } function computeMaxLength(cm) { var d = cm.display, doc = cm.doc; d.maxLine = getLine(doc, doc.first); d.maxLineLength = lineLength(doc, d.maxLine); d.maxLineChanged = true; doc.iter(function(line) { var len = lineLength(doc, line); if (len > d.maxLineLength) { d.maxLineLength = len; d.maxLine = line; } }); } // Make sure the gutters options contains the element // "CodeMirror-linenumbers" when the lineNumbers option is true. function setGuttersForLineNumbers(options) { var found = indexOf(options.gutters, "CodeMirror-linenumbers"); if (found == -1 && options.lineNumbers) { options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); } else if (found > -1 && !options.lineNumbers) { options.gutters = options.gutters.slice(0); options.gutters.splice(found, 1); } } // SCROLLBARS // Re-synchronize the fake scrollbars with the actual size of the // content. Optionally force a scrollTop. function updateScrollbars(cm) { var d = cm.display, docHeight = cm.doc.height; var totalHeight = docHeight + paddingVert(d); d.sizer.style.minHeight = d.heightForcer.style.top = totalHeight + "px"; d.gutters.style.height = Math.max(totalHeight, d.scroller.clientHeight - scrollerCutOff) + "px"; var scrollHeight = Math.max(totalHeight, d.scroller.scrollHeight); var needsH = d.scroller.scrollWidth > d.scroller.clientWidth; var needsV = scrollHeight > d.scroller.clientHeight; if (needsV) { d.scrollbarV.style.display = "block"; d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0"; // A bug in IE8 can cause this value to be negative, so guard it. d.scrollbarV.firstChild.style.height = Math.max(0, scrollHeight - d.scroller.clientHeight + d.scrollbarV.clientHeight) + "px"; } else { d.scrollbarV.style.display = ""; d.scrollbarV.firstChild.style.height = "0"; } if (needsH) { d.scrollbarH.style.display = "block"; d.scrollbarH.style.right = needsV ? scrollbarWidth(d.measure) + "px" : "0"; d.scrollbarH.firstChild.style.width = (d.scroller.scrollWidth - d.scroller.clientWidth + d.scrollbarH.clientWidth) + "px"; } else { d.scrollbarH.style.display = ""; d.scrollbarH.firstChild.style.width = "0"; } if (needsH && needsV) { d.scrollbarFiller.style.display = "block"; d.scrollbarFiller.style.height = d.scrollbarFiller.style.width = scrollbarWidth(d.measure) + "px"; } else d.scrollbarFiller.style.display = ""; if (needsH && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { d.gutterFiller.style.display = "block"; d.gutterFiller.style.height = scrollbarWidth(d.measure) + "px"; d.gutterFiller.style.width = d.gutters.offsetWidth + "px"; } else d.gutterFiller.style.display = ""; if (mac_geLion && scrollbarWidth(d.measure) === 0) { d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = mac_geMountainLion ? "18px" : "12px"; var barMouseDown = function(e) { if (e_target(e) != d.scrollbarV && e_target(e) != d.scrollbarH) operation(cm, onMouseDown)(e); }; on(d.scrollbarV, "mousedown", barMouseDown); on(d.scrollbarH, "mousedown", barMouseDown); } } function visibleLines(display, doc, viewPort) { var top = display.scroller.scrollTop, height = display.wrapper.clientHeight; if (typeof viewPort == "number") top = viewPort; else if (viewPort) {top = viewPort.top; height = viewPort.bottom - viewPort.top;} top = Math.floor(top - paddingTop(display)); var bottom = Math.ceil(top + height); return {from: lineAtHeight(doc, top), to: lineAtHeight(doc, bottom)}; } // LINE NUMBERS function alignHorizontally(cm) { var display = cm.display; if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; var gutterW = display.gutters.offsetWidth, l = comp + "px"; for (var n = display.lineDiv.firstChild; n; n = n.nextSibling) if (n.alignable) { for (var i = 0, a = n.alignable; i < a.length; ++i) a[i].style.left = l; } if (cm.options.fixedGutter) display.gutters.style.left = (comp + gutterW) + "px"; } function maybeUpdateLineNumberWidth(cm) { if (!cm.options.lineNumbers) return false; var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; if (last.length != display.lineNumChars) { var test = display.measure.appendChild(elt("div", [elt("div", last)], "CodeMirror-linenumber CodeMirror-gutter-elt")); var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; display.lineGutter.style.width = ""; display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding); display.lineNumWidth = display.lineNumInnerWidth + padding; display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; display.lineGutter.style.width = display.lineNumWidth + "px"; return true; } return false; } function lineNumberFor(options, i) { return String(options.lineNumberFormatter(i + options.firstLineNumber)); } function compensateForHScroll(display) { return getRect(display.scroller).left - getRect(display.sizer).left; } // DISPLAY DRAWING function updateDisplay(cm, changes, viewPort, forced) { var oldFrom = cm.display.showingFrom, oldTo = cm.display.showingTo, updated; var visible = visibleLines(cm.display, cm.doc, viewPort); for (var first = true;; first = false) { var oldWidth = cm.display.scroller.clientWidth; if (!updateDisplayInner(cm, changes, visible, forced)) break; updated = true; changes = []; updateSelection(cm); updateScrollbars(cm); 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 = Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, typeof viewPort == "number" ? viewPort : viewPort.top); visible = visibleLines(cm.display, cm.doc, viewPort); if (visible.from >= cm.display.showingFrom && visible.to <= cm.display.showingTo) break; } if (updated) { signalLater(cm, "update", cm); if (cm.display.showingFrom != oldFrom || cm.display.showingTo != oldTo) signalLater(cm, "viewportChange", cm, cm.display.showingFrom, cm.display.showingTo); } return updated; } // Uses a set of changes plus the current scroll position to // determine which DOM updates have to be made, and makes the // updates. function updateDisplayInner(cm, changes, visible, forced) { var display = cm.display, doc = cm.doc; if (!display.wrapper.offsetWidth) { display.showingFrom = display.showingTo = doc.first; display.viewOffset = 0; return; } // Bail out if the visible area is already rendered and nothing changed. if (!forced && changes.length == 0 && visible.from > display.showingFrom && visible.to < display.showingTo) return; if (maybeUpdateLineNumberWidth(cm)) changes = [{from: doc.first, to: doc.first + doc.size}]; var gutterW = display.sizer.style.marginLeft = display.gutters.offsetWidth + "px"; display.scrollbarH.style.left = cm.options.fixedGutter ? gutterW : "0"; // Used to determine which lines need their line numbers updated var positionsChangedFrom = Infinity; if (cm.options.lineNumbers) for (var i = 0; i < changes.length; ++i) if (changes[i].diff && changes[i].from < positionsChangedFrom) { positionsChangedFrom = changes[i].from; } 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); if (display.showingFrom < from && from - display.showingFrom < 20) from = Math.max(doc.first, display.showingFrom); if (display.showingTo > to && display.showingTo - to < 20) to = Math.min(end, display.showingTo); if (sawCollapsedSpans) { from = lineNo(visualLine(doc, getLine(doc, from))); while (to < end && lineIsHidden(doc, getLine(doc, to))) ++to; } // Create a range of theoretically intact lines, and punch holes // in that using the change info. var intact = [{from: Math.max(display.showingFrom, doc.first), to: Math.min(display.showingTo, end)}]; if (intact[0].from >= intact[0].to) intact = []; else intact = computeIntact(intact, changes); // When merged lines are present, we might have to reduce the // intact ranges because changes in continued fragments of the // intact lines do require the lines to be redrawn. if (sawCollapsedSpans) for (var i = 0; i < intact.length; ++i) { var range = intact[i], merged; while (merged = collapsedSpanAtEnd(getLine(doc, range.to - 1))) { var newTo = merged.find().from.line; if (newTo > range.from) range.to = newTo; else { intact.splice(i--, 1); break; } } } // Clip off the parts that won't be visible var intactLines = 0; for (var i = 0; i < intact.length; ++i) { var range = intact[i]; if (range.from < from) range.from = from; if (range.to > to) range.to = to; if (range.from >= range.to) intact.splice(i--, 1); else intactLines += range.to - range.from; } if (!forced && intactLines == to - from && from == display.showingFrom && to == display.showingTo) { updateViewOffset(cm); return; } intact.sort(function(a, b) {return a.from - b.from;}); // Avoid crashing on IE's "unspecified error" when in iframes try { var focused = document.activeElement; } catch(e) {} if (intactLines < (to - from) * .7) display.lineDiv.style.display = "none"; patchDisplay(cm, from, to, intact, positionsChangedFrom); display.lineDiv.style.display = ""; if (focused && document.activeElement != focused && focused.offsetHeight) focused.focus(); var different = from != display.showingFrom || to != display.showingTo || display.lastSizeC != display.wrapper.clientHeight; // This is just a bogus formula that detects when the editor is // resized or the font size changes. if (different) { display.lastSizeC = display.wrapper.clientHeight; startWorker(cm, 400); } display.showingFrom = from; display.showingTo = to; display.gutters.style.height = ""; updateHeightsInViewport(cm); updateViewOffset(cm); return true; } function updateHeightsInViewport(cm) { var display = cm.display; var prevBottom = display.lineDiv.offsetTop; for (var node = display.lineDiv.firstChild, height; node; node = node.nextSibling) if (node.lineObj) { if (ie_lt8) { var bot = node.offsetTop + node.offsetHeight; height = bot - prevBottom; prevBottom = bot; } else { var box = getRect(node); height = box.bottom - box.top; } var diff = node.lineObj.height - height; if (height < 2) height = textHeight(display); if (diff > .001 || diff < -.001) { updateLineHeight(node.lineObj, height); var widgets = node.lineObj.widgets; if (widgets) for (var i = 0; i < widgets.length; ++i) widgets[i].height = widgets[i].node.offsetHeight; } } } function updateViewOffset(cm) { var off = cm.display.viewOffset = heightAtLine(cm, getLine(cm.doc, cm.display.showingFrom)); // Position the mover div to align with the current virtual scroll position cm.display.mover.style.top = off + "px"; } function computeIntact(intact, changes) { for (var i = 0, l = changes.length || 0; i < l; ++i) { var change = changes[i], intact2 = [], diff = change.diff || 0; for (var j = 0, l2 = intact.length; j < l2; ++j) { var range = intact[j]; if (change.to <= range.from && change.diff) { intact2.push({from: range.from + diff, to: range.to + diff}); } else if (change.to <= range.from || change.from >= range.to) { intact2.push(range); } else { if (change.from > range.from) intact2.push({from: range.from, to: change.from}); if (change.to < range.to) intact2.push({from: change.to + diff, to: range.to + diff}); } } intact = intact2; } return intact; } function getDimensions(cm) { var d = cm.display, left = {}, width = {}; for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { left[cm.options.gutters[i]] = n.offsetLeft; width[cm.options.gutters[i]] = n.offsetWidth; } return {fixedPos: compensateForHScroll(d), gutterTotalWidth: d.gutters.offsetWidth, gutterLeft: left, gutterWidth: width, wrapperWidth: d.wrapper.clientWidth}; } function patchDisplay(cm, from, to, intact, updateNumbersFrom) { var dims = getDimensions(cm); var display = cm.display, lineNumbers = cm.options.lineNumbers; if (!intact.length && (!webkit || !cm.display.currentWheelTarget)) removeChildren(display.lineDiv); var container = display.lineDiv, cur = container.firstChild; function rm(node) { var next = node.nextSibling; if (webkit && mac && cm.display.currentWheelTarget == node) { node.style.display = "none"; node.lineObj = null; } else { node.parentNode.removeChild(node); } return next; } var nextIntact = intact.shift(), lineN = from; cm.doc.iter(from, to, function(line) { if (nextIntact && nextIntact.to == lineN) nextIntact = intact.shift(); if (lineIsHidden(cm.doc, line)) { if (line.height != 0) updateLineHeight(line, 0); if (line.widgets && cur && cur.previousSibling) for (var i = 0; i < line.widgets.length; ++i) { var w = line.widgets[i]; if (w.showIfHidden) { var prev = cur.previousSibling; if (/pre/i.test(prev.nodeName)) { var wrap = elt("div", null, null, "position: relative"); prev.parentNode.replaceChild(wrap, prev); wrap.appendChild(prev); prev = wrap; } var wnode = prev.appendChild(elt("div", [w.node], "CodeMirror-linewidget")); if (!w.handleMouseEvents) wnode.ignoreEvents = true; positionLineWidget(w, wnode, prev, dims); } } } else if (nextIntact && nextIntact.from <= lineN && nextIntact.to > lineN) { // This line is intact. Skip to the actual node. Update its // line number if needed. while (cur.lineObj != line) cur = rm(cur); if (lineNumbers && updateNumbersFrom <= lineN && cur.lineNumber) setTextContent(cur.lineNumber, lineNumberFor(cm.options, lineN)); cur = cur.nextSibling; } else { // For lines with widgets, make an attempt to find and reuse // the existing element, so that widgets aren't needlessly // removed and re-inserted into the dom if (line.widgets) for (var j = 0, search = cur, reuse; search && j < 20; ++j, search = search.nextSibling) if (search.lineObj == line && /div/i.test(search.nodeName)) { reuse = search; break; } // This line needs to be generated. var lineNode = buildLineElement(cm, line, lineN, dims, reuse); if (lineNode != reuse) { container.insertBefore(lineNode, cur); } else { while (cur != reuse) cur = rm(cur); cur = cur.nextSibling; } lineNode.lineObj = line; } ++lineN; }); while (cur) cur = rm(cur); } function buildLineElement(cm, line, lineNo, dims, reuse) { var built = buildLineContent(cm, line), lineElement = built.pre; var markers = line.gutterMarkers, display = cm.display, wrap; var bgClass = built.bgClass ? built.bgClass + " " + (line.bgClass || "") : line.bgClass; if (!cm.options.lineNumbers && !markers && !bgClass && !line.wrapClass && !line.widgets) return lineElement; // Lines with gutter elements, widgets or a background class need // to be wrapped again, and have the extra elements added to the // wrapper div if (reuse) { reuse.alignable = null; var isOk = true, widgetsSeen = 0, insertBefore = null; for (var n = reuse.firstChild, next; n; n = next) { next = n.nextSibling; if (!/\bCodeMirror-linewidget\b/.test(n.className)) { reuse.removeChild(n); } else { for (var i = 0; i < line.widgets.length; ++i) { var widget = line.widgets[i]; if (widget.node == n.firstChild) { if (!widget.above && !insertBefore) insertBefore = n; positionLineWidget(widget, n, reuse, dims); ++widgetsSeen; break; } } if (i == line.widgets.length) { isOk = false; break; } } } reuse.insertBefore(lineElement, insertBefore); if (isOk && widgetsSeen == line.widgets.length) { wrap = reuse; reuse.className = line.wrapClass || ""; } } if (!wrap) { wrap = elt("div", null, line.wrapClass, "position: relative"); wrap.appendChild(lineElement); } // Kludge to make sure the styled element lies behind the selection (by z-index) if (bgClass) wrap.insertBefore(elt("div", null, bgClass + " CodeMirror-linebackground"), wrap.firstChild); if (cm.options.lineNumbers || markers) { var gutterWrap = wrap.insertBefore(elt("div", null, "CodeMirror-gutter-wrapper", "position: absolute; left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"), lineElement); if (cm.options.fixedGutter) (wrap.alignable || (wrap.alignable = [])).push(gutterWrap); if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) wrap.lineNumber = gutterWrap.appendChild( elt("div", lineNumberFor(cm.options, lineNo), "CodeMirror-linenumber CodeMirror-gutter-elt", "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " + display.lineNumInnerWidth + "px")); if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) { var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; if (found) gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); } } if (ie_lt8) wrap.style.zIndex = 2; if (line.widgets && wrap != reuse) for (var i = 0, ws = line.widgets; i < ws.length; ++i) { var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); if (!widget.handleMouseEvents) node.ignoreEvents = true; positionLineWidget(widget, node, wrap, dims); if (widget.above) wrap.insertBefore(node, cm.options.lineNumbers && line.height != 0 ? gutterWrap : lineElement); else wrap.appendChild(node); signalLater(widget, "redraw"); } return wrap; } function positionLineWidget(widget, node, wrap, dims) { if (widget.noHScroll) { (wrap.alignable || (wrap.alignable = [])).push(node); var width = dims.wrapperWidth; node.style.left = dims.fixedPos + "px"; if (!widget.coverGutter) { width -= dims.gutterTotalWidth; node.style.paddingLeft = dims.gutterTotalWidth + "px"; } node.style.width = width + "px"; } if (widget.coverGutter) { node.style.zIndex = 5; node.style.position = "relative"; if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; } } // SELECTION / CURSOR function updateSelection(cm) { var display = cm.display; var collapsed = posEq(cm.doc.sel.from, cm.doc.sel.to); if (collapsed || cm.options.showCursorWhenSelecting) updateSelectionCursor(cm); else display.cursor.style.display = display.otherCursor.style.display = "none"; if (!collapsed) updateSelectionRange(cm); else display.selectionDiv.style.display = "none"; // Move the hidden textarea near the cursor to prevent scrolling artifacts if (cm.options.moveInputWithCursor) { var headPos = cursorCoords(cm, cm.doc.sel.head, "div"); var wrapOff = getRect(display.wrapper), lineOff = getRect(display.lineDiv); display.inputDiv.style.top = Math.max(0, Math.min(display.wrapper.clientHeight - 10, headPos.top + lineOff.top - wrapOff.top)) + "px"; display.inputDiv.style.left = Math.max(0, Math.min(display.wrapper.clientWidth - 10, headPos.left + lineOff.left - wrapOff.left)) + "px"; } } // No selection, plain cursor function updateSelectionCursor(cm) { var display = cm.display, pos = cursorCoords(cm, cm.doc.sel.head, "div"); display.cursor.style.left = pos.left + "px"; display.cursor.style.top = pos.top + "px"; display.cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; display.cursor.style.display = ""; if (pos.other) { display.otherCursor.style.display = ""; display.otherCursor.style.left = pos.other.left + "px"; display.otherCursor.style.top = pos.other.top + "px"; display.otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; } else { display.otherCursor.style.display = "none"; } } // Highlight selection function updateSelectionRange(cm) { var display = cm.display, doc = cm.doc, sel = cm.doc.sel; var fragment = document.createDocumentFragment(); var padding = paddingH(cm.display), leftSide = padding.left, rightSide = display.lineSpace.offsetWidth - padding.right; function add(left, top, width, bottom) { if (top < 0) top = 0; fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) + "px; height: " + (bottom - top) + "px")); } function drawForLine(line, fromArg, toArg) { var lineObj = getLine(doc, line); var lineLen = lineObj.text.length; var start, end; function coords(ch, bias) { return charCoords(cm, Pos(line, ch), "div", lineObj, bias); } iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { var leftPos = coords(from, "left"), rightPos, left, right; if (from == to) { rightPos = leftPos; left = right = leftPos.left; } else { rightPos = coords(to - 1, "right"); if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } left = leftPos.left; right = rightPos.right; } if (fromArg == null && from == 0) left = leftSide; if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part add(left, leftPos.top, null, leftPos.bottom); left = leftSide; if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); } if (toArg == null && to == lineLen) right = rightSide; if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) start = leftPos; if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) end = rightPos; if (left < leftSide + 1) left = leftSide; add(left, rightPos.top, right - left, rightPos.bottom); }); return {start: start, end: end}; } if (sel.from.line == sel.to.line) { drawForLine(sel.from.line, sel.from.ch, sel.to.ch); } else { var fromLine = getLine(doc, sel.from.line), toLine = getLine(doc, sel.to.line); var singleVLine = visualLine(doc, fromLine) == visualLine(doc, toLine); var leftEnd = drawForLine(sel.from.line, sel.from.ch, singleVLine ? fromLine.text.length : null).end; var rightStart = drawForLine(sel.to.line, singleVLine ? 0 : null, sel.to.ch).start; if (singleVLine) { if (leftEnd.top < rightStart.top - 2) { add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); } else { add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); } } if (leftEnd.bottom < rightStart.top) add(leftSide, leftEnd.bottom, null, rightStart.top); } removeChildrenAndAdd(display.selectionDiv, fragment); display.selectionDiv.style.display = ""; } // Cursor-blinking function restartBlink(cm) { if (!cm.state.focused) return; var display = cm.display; clearInterval(display.blinker); var on = true; display.cursor.style.visibility = display.otherCursor.style.visibility = ""; if (cm.options.cursorBlinkRate > 0) display.blinker = setInterval(function() { display.cursor.style.visibility = display.otherCursor.style.visibility = (on = !on) ? "" : "hidden"; }, cm.options.cursorBlinkRate); } // HIGHLIGHT WORKER function startWorker(cm, time) { if (cm.doc.mode.startState && cm.doc.frontier < cm.display.showingTo) cm.state.highlight.set(time, bind(highlightWorker, cm)); } function highlightWorker(cm) { var doc = cm.doc; if (doc.frontier < doc.first) doc.frontier = doc.first; if (doc.frontier >= cm.display.showingTo) return; var end = +new Date + cm.options.workTime; var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); var changed = [], prevChange; doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.showingTo + 500), function(line) { if (doc.frontier >= cm.display.showingFrom) { // Visible var oldStyles = line.styles; line.styles = highlightLine(cm, line, state, true); var ischange = !oldStyles || oldStyles.length != line.styles.length; for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; if (ischange) { if (prevChange && prevChange.end == doc.frontier) prevChange.end++; else changed.push(prevChange = {start: doc.frontier, end: doc.frontier + 1}); } line.stateAfter = copyState(doc.mode, state); } else { processLine(cm, line.text, state); line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; } ++doc.frontier; if (+new Date > end) { startWorker(cm, cm.options.workDelay); return true; } }); if (changed.length) operation(cm, function() { for (var i = 0; i < changed.length; ++i) regChange(this, changed[i].start, changed[i].end); })(); } // Finds the line to start with when starting a parse. Tries to // find a line with a stateAfter, so that it can start with a // valid state. If that fails, it returns the line with the // smallest indentation, which tends to need the least context to // parse correctly. function findStartLine(cm, n, precise) { var minindent, minline, doc = cm.doc; var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); for (var search = n; search > lim; --search) { if (search <= doc.first) return doc.first; var line = getLine(doc, search - 1); if (line.stateAfter && (!precise || search <= doc.frontier)) return search; var indented = countColumn(line.text, null, cm.options.tabSize); if (minline == null || minindent > indented) { minline = search - 1; minindent = indented; } } return minline; } function getStateBefore(cm, n, precise) { var doc = cm.doc, display = cm.display; if (!doc.mode.startState) return true; var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; if (!state) state = startState(doc.mode); else state = copyState(doc.mode, state); doc.iter(pos, n, function(line) { processLine(cm, line.text, state); var save = pos == n - 1 || pos % 5 == 0 || pos >= display.showingFrom && pos < display.showingTo; line.stateAfter = save ? copyState(doc.mode, state) : null; ++pos; }); if (precise) doc.frontier = pos; return state; } // POSITION MEASUREMENT function paddingTop(display) {return display.lineSpace.offsetTop;} function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} function paddingH(display) { if (display.cachedPaddingH) return display.cachedPaddingH; var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; return display.cachedPaddingH = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; } function measureChar(cm, line, ch, data, bias) { var dir = -1; data = data || measureLine(cm, line); if (data.crude) { var left = data.left + ch * data.width; return {left: left, right: left + data.width, top: data.top, bottom: data.bottom}; } for (var pos = ch;; pos += dir) { var r = data[pos]; if (r) break; if (dir < 0 && pos == 0) dir = 1; } bias = pos > ch ? "left" : pos < ch ? "right" : bias; if (bias == "left" && r.leftSide) r = r.leftSide; else if (bias == "right" && r.rightSide) r = r.rightSide; return {left: pos < ch ? r.right : r.left, right: pos > ch ? r.left : r.right, top: r.top, bottom: r.bottom}; } function findCachedMeasurement(cm, line) { var cache = cm.display.measureLineCache; for (var i = 0; i < cache.length; ++i) { var memo = cache[i]; if (memo.text == line.text && memo.markedSpans == line.markedSpans && cm.display.scroller.clientWidth == memo.width && memo.classes == line.textClass + "|" + line.wrapClass) return memo; } } function clearCachedMeasurement(cm, line) { var exists = findCachedMeasurement(cm, line); if (exists) exists.text = exists.measure = exists.markedSpans = null; } function measureLine(cm, line) { // First look in the cache var cached = findCachedMeasurement(cm, line); if (cached) return cached.measure; // Failing that, recompute and store result in cache var measure = measureLineInner(cm, line); var cache = cm.display.measureLineCache; var memo = {text: line.text, width: cm.display.scroller.clientWidth, markedSpans: line.markedSpans, measure: measure, classes: line.textClass + "|" + line.wrapClass}; if (cache.length == 16) cache[++cm.display.measureLineCachePos % 16] = memo; else cache.push(memo); return measure; } function measureLineInner(cm, line) { if (!cm.options.lineWrapping && line.text.length >= cm.options.crudeMeasuringFrom) return crudelyMeasureLine(cm, line); var display = cm.display, measure = emptyArray(line.text.length); var pre = buildLineContent(cm, line, measure, true).pre; // IE does not cache element positions of inline elements between // calls to getBoundingClientRect. This makes the loop below, // which gathers the positions of all the characters on the line, // do an amount of layout work quadratic to the number of // characters. When line wrapping is off, we try to improve things // by first subdividing the line into a bunch of inline blocks, so // that IE can reuse most of the layout information from caches // for those blocks. This does interfere with line wrapping, so it // doesn't work when wrapping is on, but in that case the // situation is slightly better, since IE does cache line-wrapping // information and only recomputes per-line. if (old_ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) { var fragment = document.createDocumentFragment(); var chunk = 10, n = pre.childNodes.length; for (var i = 0, chunks = Math.ceil(n / chunk); i < chunks; ++i) { var wrap = elt("div", null, null, "display: inline-block"); for (var j = 0; j < chunk && n; ++j) { wrap.appendChild(pre.firstChild); --n; } fragment.appendChild(wrap); } pre.appendChild(fragment); } removeChildrenAndAdd(display.measure, pre); var outer = getRect(display.lineDiv); var vranges = [], data = emptyArray(line.text.length), maxBot = pre.offsetHeight; // Work around an IE7/8 bug where it will sometimes have randomly // replaced our pre with a clone at this point. if (ie_lt9 && display.measure.first != pre) removeChildrenAndAdd(display.measure, pre); function measureRect(rect) { var top = rect.top - outer.top, bot = rect.bottom - outer.top; if (bot > maxBot) bot = maxBot; if (top < 0) top = 0; for (var i = vranges.length - 2; i >= 0; i -= 2) { var rtop = vranges[i], rbot = vranges[i+1]; if (rtop > bot || rbot < top) continue; if (rtop <= top && rbot >= bot || top <= rtop && bot >= rbot || Math.min(bot, rbot) - Math.max(top, rtop) >= (bot - top) >> 1) { vranges[i] = Math.min(top, rtop); vranges[i+1] = Math.max(bot, rbot); break; } } if (i < 0) { i = vranges.length; vranges.push(top, bot); } return {left: rect.left - outer.left, right: rect.right - outer.left, top: i, bottom: null}; } function finishRect(rect) { rect.bottom = vranges[rect.top+1]; rect.top = vranges[rect.top]; } for (var i = 0, cur; i < measure.length; ++i) if (cur = measure[i]) { var node = cur, rect = null; // A widget might wrap, needs special care if (/\bCodeMirror-widget\b/.test(cur.className) && cur.getClientRects) { if (cur.firstChild.nodeType == 1) node = cur.firstChild; var rects = node.getClientRects(); if (rects.length > 1) { rect = data[i] = measureRect(rects[0]); rect.rightSide = measureRect(rects[rects.length - 1]); } } if (!rect) rect = data[i] = measureRect(getRect(node)); if (cur.measureRight) rect.right = getRect(cur.measureRight).left - outer.left; if (cur.leftSide) rect.leftSide = measureRect(getRect(cur.leftSide)); } removeChildren(cm.display.measure); for (var i = 0, cur; i < data.length; ++i) if (cur = data[i]) { finishRect(cur); if (cur.leftSide) finishRect(cur.leftSide); if (cur.rightSide) finishRect(cur.rightSide); } return data; } function crudelyMeasureLine(cm, line) { var copy = new Line(line.text.slice(0, 100), null); if (line.textClass) copy.textClass = line.textClass; var measure = measureLineInner(cm, copy); var left = measureChar(cm, copy, 0, measure, "left"); var right = measureChar(cm, copy, 99, measure, "right"); return {crude: true, top: left.top, left: left.left, bottom: left.bottom, width: (right.right - left.left) / 100}; } function measureLineWidth(cm, line) { var hasBadSpan = false; if (line.markedSpans) for (var i = 0; i < line.markedSpans; ++i) { var sp = line.markedSpans[i]; if (sp.collapsed && (sp.to == null || sp.to == line.text.length)) hasBadSpan = true; } var cached = !hasBadSpan && findCachedMeasurement(cm, line); if (cached || line.text.length >= cm.options.crudeMeasuringFrom) return measureChar(cm, line, line.text.length, cached && cached.measure, "right").right; var pre = buildLineContent(cm, line, null, true).pre; var end = pre.appendChild(zeroWidthElement(cm.display.measure)); removeChildrenAndAdd(cm.display.measure, pre); var rect = getRect(end); if (rect.right == 0 && rect.bottom == 0) { end = pre.appendChild(elt("span", "\u00a0")); rect = getRect(end); } return