codemirror
Version:
In-browser code editing made bearable
1,176 lines (1,062 loc) • 243 kB
JavaScript
// 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