diff --git a/rhodecode/public/css/codemirror.css b/rhodecode/public/css/codemirror.css --- a/rhodecode/public/css/codemirror.css +++ b/rhodecode/public/css/codemirror.css @@ -9,8 +9,7 @@ } .CodeMirror-scroll { - overflow-x: auto; - overflow-y: hidden; + overflow: auto; height: 300px; /* This is needed to prevent an IE[67] bug where the scrolled content is visible outside of the scrolling box. */ @@ -20,13 +19,11 @@ /* Vertical scrollbar */ .CodeMirror-scrollbar { - float: right; + position: absolute; + right: 0; top: 0; overflow-x: hidden; overflow-y: scroll; - - /* This corrects for the 1px gap introduced to the left of the scrollbar - by the rule for .CodeMirror-scrollbar-inner. */ - margin-left: -1px; + z-index: 5; } .CodeMirror-scrollbar-inner { /* This needs to have a nonzero width in order for the scrollbar to appear @@ -62,16 +59,13 @@ text-align: right; padding: .4em .2em .4em .4em; white-space: pre !important; + cursor: default; } .CodeMirror-lines { padding: .4em; white-space: pre; cursor: text; } -.CodeMirror-lines * { - /* Necessary for throw-scrolling to decelerate properly on Safari. */ - pointer-events: none; -} .CodeMirror pre { -moz-border-radius: 0; @@ -151,7 +145,7 @@ div.CodeMirror-selected { background: #d .cm-s-default span.cm-error {color: #f00;} .cm-s-default span.cm-qualifier {color: #555;} .cm-s-default span.cm-builtin {color: #30a;} -.cm-s-default span.cm-bracket {color: #cc7;} +.cm-s-default span.cm-bracket {color: #997;} .cm-s-default span.cm-tag {color: #170;} .cm-s-default span.cm-attribute {color: #00c;} .cm-s-default span.cm-header {color: blue;} @@ -164,5 +158,16 @@ span.cm-em {font-style: italic;} span.cm-emstrong {font-style: italic; font-weight: bold;} span.cm-link {text-decoration: underline;} +span.cm-invalidchar {color: #f00;} + div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} + +@media print { + + /* Hide the cursor when printing */ + .CodeMirror pre.CodeMirror-cursor { + visibility: hidden; + } + +} diff --git a/rhodecode/public/js/codemirror.js b/rhodecode/public/js/codemirror.js --- a/rhodecode/public/js/codemirror.js +++ b/rhodecode/public/js/codemirror.js @@ -3,7 +3,8 @@ // some utilities are defined. // CodeMirror is the only global var we claim -var CodeMirror = (function() { +window.CodeMirror = (function() { + "use strict"; // This is the function that produces an editor instance. Its // closure is used to store the editor state. function CodeMirror(place, givenOptions) { @@ -13,38 +14,33 @@ var CodeMirror = (function() { if (defaults.hasOwnProperty(opt)) options[opt] = (givenOptions && givenOptions.hasOwnProperty(opt) ? givenOptions : defaults)[opt]; + var input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em"); + input.setAttribute("wrap", "off"); input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); + // Wraps and hides input textarea + var inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The empty scrollbar content, used solely for managing the scrollbar thumb. + var scrollbarInner = elt("div", null, "CodeMirror-scrollbar-inner"); + // The vertical scrollbar. Horizontal scrolling is handled by the scroller itself. + var scrollbar = elt("div", [scrollbarInner], "CodeMirror-scrollbar"); + // DIVs containing the selection and the actual code + var lineDiv = elt("div"), 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 + var cursor = elt("pre", "\u00a0", "CodeMirror-cursor"), widthForcer = elt("pre", "\u00a0", "CodeMirror-cursor", "visibility: hidden"); + // Used to measure text size + var measure = elt("div", null, null, "position: absolute; width: 100%; height: 0px; overflow: hidden; visibility: hidden;"); + var lineSpace = elt("div", [measure, cursor, widthForcer, selectionDiv, lineDiv], null, "position: relative; z-index: 0"); + var gutterText = elt("div", null, "CodeMirror-gutter-text"), gutter = elt("div", [gutterText], "CodeMirror-gutter"); + // Moved around its parent to cover visible view + var mover = elt("div", [gutter, elt("div", [lineSpace], "CodeMirror-lines")], null, "position: relative"); + // Set to the height of the text, causes scrolling + var sizer = elt("div", [mover], null, "position: relative"); + // Provides scrolling + var scroller = elt("div", [sizer], "CodeMirror-scroll"); + scroller.setAttribute("tabIndex", "-1"); // The element in which the editor lives. - var wrapper = document.createElement("div"); - wrapper.className = "CodeMirror" + (options.lineWrapping ? " CodeMirror-wrap" : ""); - // This mess creates the base DOM structure for the editor. - wrapper.innerHTML = - '
' + - ' ' + // This must be before the scroll area because it's float-right. - '' + // Absolutely positioned blinky cursor - ' ' + // Used to force a width - '' + // DIVs containing the selection and the actual code - '
' - + line.getHTML(makeTab) + ''; + var lineElement = lineContent(line); + if (line.className) lineElement.className = line.className; // Kludge to make sure the styled element lies behind the selection (by z-index) - if (line.bgClassName) - html = '
' + html + "
' : ""), text); - for (var j = 1; j < line.height; ++j) html.push(""); + var markerElement = fragment.appendChild(elt("pre", null, marker && marker.style)); + markerElement.innerHTML = text; + for (var j = 1; j < line.height; ++j) { + markerElement.appendChild(elt("br")); + markerElement.appendChild(document.createTextNode("\u00a0")); + } if (!marker) normalNode = i; } ++i; }); gutter.style.display = "none"; - gutterText.innerHTML = html.join(""); + removeChildrenAndAdd(gutterText, fragment); // Make sure scrolling doesn't cause number gutter size to pop if (normalNode != null && options.lineNumbers) { var node = gutterText.childNodes[normalNode - showingFrom]; @@ -1230,15 +1242,15 @@ var CodeMirror = (function() { cursor.style.display = ""; selectionDiv.style.display = "none"; } else { - var sameLine = fromPos.y == toPos.y, html = ""; + var sameLine = fromPos.y == toPos.y, fragment = document.createDocumentFragment(); var clientWidth = lineSpace.clientWidth || lineSpace.offsetWidth; var clientHeight = lineSpace.clientHeight || lineSpace.offsetHeight; - function add(left, top, right, height) { + var add = function(left, top, right, height) { var rstyle = quirksMode ? "width: " + (!right ? clientWidth : clientWidth - right - left) + "px" : "right: " + right + "px"; - html += ''; - } + fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + + "px; top: " + top + "px; " + rstyle + "; height: " + height + "px")); + }; if (sel.from.ch && fromPos.y >= 0) { var right = sameLine ? clientWidth - toPos.x : 0; add(fromPos.x, fromPos.y, right, th); @@ -1249,7 +1261,7 @@ var CodeMirror = (function() { add(0, middleStart, 0, middleHeight); if ((!sameLine || !sel.from.ch) && toPos.y < clientHeight - .5 * th) add(0, toPos.y, clientWidth - toPos.x, th); - selectionDiv.innerHTML = html; + removeChildrenAndAdd(selectionDiv, fragment); cursor.style.display = "none"; selectionDiv.style.display = ""; } @@ -1381,24 +1393,34 @@ var CodeMirror = (function() { else replaceRange("", sel.from, findPosH(dir, unit)); userSelChange = true; } - var goalColumn = null; function moveV(dir, unit) { var dist = 0, pos = localCoords(sel.inverted ? sel.from : sel.to, true); if (goalColumn != null) pos.x = goalColumn; - if (unit == "page") dist = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight); - else if (unit == "line") dist = textHeight(); - var target = coordsChar(pos.x, pos.y + dist * dir + 2); + if (unit == "page") { + var screen = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight); + var target = coordsChar(pos.x, pos.y + screen * dir); + } else if (unit == "line") { + var th = textHeight(); + var target = coordsChar(pos.x, pos.y + .5 * th + dir * th); + } if (unit == "page") scrollbar.scrollTop += localCoords(target, true).y - pos.y; setCursor(target.line, target.ch, true); goalColumn = pos.x; } - function selectWordAt(pos) { + function findWordAt(pos) { var line = getLine(pos.line).text; var start = pos.ch, end = pos.ch; - while (start > 0 && isWordChar(line.charAt(start - 1))) --start; - while (end < line.length && isWordChar(line.charAt(end))) ++end; - setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end}); + if (line) { + if (pos.after === false || end == line.length) --start; else ++end; + var startChar = line.charAt(start); + var check = isWordChar(startChar) ? isWordChar : + /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} : + function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + } + return {from: {line: pos.line, ch: start}, to: {line: pos.line, ch: end}}; } function selectLine(line) { setSelectionUser({line: line, ch: 0}, clipPos({line: line + 1, ch: 0})); @@ -1431,24 +1453,20 @@ var CodeMirror = (function() { indentation = Math.max(0, indentation); var diff = indentation - curSpace; - if (!diff) { - if (sel.from.line != n && sel.to.line != n) return; - var indentString = curSpaceString; - } else { - var indentString = "", pos = 0; - if (options.indentWithTabs) - for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";} - while (pos < indentation) {++pos; indentString += " ";} - } + var indentString = "", pos = 0; + if (options.indentWithTabs) + for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";} + if (pos < indentation) indentString += spaceStr(indentation - pos); - replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length}); + if (indentString != curSpaceString) + replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length}); } function loadMode() { mode = CodeMirror.getMode(options, options.mode); doc.iter(0, doc.size, function(line) { line.stateAfter = null; }); - work = [0]; - startWorker(); + frontier = 0; + startWorker(100); } function gutterChanged() { var visible = options.gutter || options.lineNumbers; @@ -1465,24 +1483,16 @@ var CodeMirror = (function() { var guess = Math.ceil(line.text.length / perLine) || 1; if (guess != 1) updateLineHeight(line, guess); }); - lineSpace.style.width = code.style.width = ""; - widthForcer.style.left = ""; + lineSpace.style.minWidth = widthForcer.style.left = ""; } else { wrapper.className = wrapper.className.replace(" CodeMirror-wrap", ""); - maxLine = ""; maxLineChanged = true; + computeMaxLength(); doc.iter(0, doc.size, function(line) { if (line.height != 1 && !line.hidden) updateLineHeight(line, 1); - if (line.text.length > maxLine.length) maxLine = line.text; }); } changes.push({from: 0, to: doc.size}); } - function makeTab(col) { - var w = options.tabSize - col % options.tabSize, cached = tabCache[w]; - if (cached) return cached; - for (var str = '', i = 0; i < w; ++i) str += " "; - return (tabCache[w] = {html: str + "", width: w}); - } function themeChanged() { scroller.className = scroller.className.replace(/\s*cm-s-\S+/g, "") + options.theme.replace(/(^|\s)\s*/g, " cm-s-"); @@ -1493,74 +1503,71 @@ var CodeMirror = (function() { (style ? " cm-keymap-" + style : ""); } - function TextMarker() { this.set = []; } + function TextMarker(type, style) { this.lines = []; this.type = type; if (style) this.style = style; } TextMarker.prototype.clear = operation(function() { var min = Infinity, max = -Infinity; - for (var i = 0, e = this.set.length; i < e; ++i) { - var line = this.set[i], mk = line.marked; - if (!mk || !line.parent) continue; - var lineN = lineNo(line); - min = Math.min(min, lineN); max = Math.max(max, lineN); - for (var j = 0; j < mk.length; ++j) - if (mk[j].marker == this) mk.splice(j--, 1); + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this, true); + if (span.from != null || span.to != null) { + var lineN = lineNo(line); + min = Math.min(min, lineN); max = Math.max(max, lineN); + } } if (min != Infinity) changes.push({from: min, to: max + 1}); + this.lines.length = 0; }); TextMarker.prototype.find = function() { var from, to; - for (var i = 0, e = this.set.length; i < e; ++i) { - var line = this.set[i], mk = line.marked; - for (var j = 0; j < mk.length; ++j) { - var mark = mk[j]; - if (mark.marker == this) { - if (mark.from != null || mark.to != null) { - var found = lineNo(line); - if (found != null) { - if (mark.from != null) from = {line: found, ch: mark.from}; - if (mark.to != null) to = {line: found, ch: mark.to}; - } - } - } + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null || span.to != null) { + var found = lineNo(line); + if (span.from != null) from = {line: found, ch: span.from}; + if (span.to != null) to = {line: found, ch: span.to}; } } - return {from: from, to: to}; + if (this.type == "bookmark") return from; + return from && {from: from, to: to}; }; - function markText(from, to, className) { + function markText(from, to, className, options) { from = clipPos(from); to = clipPos(to); - var tm = new TextMarker(); - if (!posLess(from, to)) return tm; - function add(line, from, to, className) { - getLine(line).addMark(new MarkedText(from, to, className, tm)); - } - if (from.line == to.line) add(from.line, from.ch, to.ch, className); - else { - add(from.line, from.ch, null, className); - for (var i = from.line + 1, e = to.line; i < e; ++i) - add(i, null, null, className); - add(to.line, null, to.ch, className); - } + var marker = new TextMarker("range", className); + if (options) for (var opt in options) if (options.hasOwnProperty(opt)) + marker[opt] = options[opt]; + var curLine = from.line; + doc.iter(curLine, to.line + 1, function(line) { + var span = {from: curLine == from.line ? from.ch : null, + to: curLine == to.line ? to.ch : null, + marker: marker}; + (line.markedSpans || (line.markedSpans = [])).push(span); + marker.lines.push(line); + ++curLine; + }); changes.push({from: from.line, to: to.line + 1}); - return tm; + return marker; } function setBookmark(pos) { pos = clipPos(pos); - var bm = new Bookmark(pos.ch); - getLine(pos.line).addMark(bm); - return bm; + var marker = new TextMarker("bookmark"), line = getLine(pos.line); + var span = {from: pos.ch, to: pos.ch, marker: marker}; + (line.markedSpans || (line.markedSpans = [])).push(span); + marker.lines.push(line); + return marker; } function findMarksAt(pos) { pos = clipPos(pos); - var markers = [], marked = getLine(pos.line).marked; - if (!marked) return markers; - for (var i = 0, e = marked.length; i < e; ++i) { - var m = marked[i]; - if ((m.from == null || m.from <= pos.ch) && - (m.to == null || m.to >= pos.ch)) - markers.push(m.marker || m); + var markers = [], spans = getLine(pos.line).markedSpans; + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + markers.push(span.marker); } return markers; } @@ -1600,11 +1607,10 @@ var CodeMirror = (function() { if (line.hidden != hidden) { line.hidden = hidden; if (!options.lineWrapping) { - var l = line.text; - if (hidden && l.length == maxLine.length) { + if (hidden && line.text.length == maxLine.text.length) { updateMaxLine = true; - } else if (!hidden && l.length > maxLine.length) { - maxLine = l; maxWidth = null; updateMaxLine = false; + } else if (!hidden && line.text.length > maxLine.text.length) { + maxLine = line; updateMaxLine = false; } } updateLineHeight(line, hidden ? 0 : 1); @@ -1636,53 +1642,18 @@ var CodeMirror = (function() { markerClass: marker && marker.style, lineClass: line.className, bgClass: line.bgClassName}; } - function stringWidth(str) { - measure.innerHTML = "
"); - html.push("x
"; - measure.firstChild.firstChild.firstChild.nodeValue = str; - return measure.firstChild.firstChild.offsetWidth || 10; - } - // These are used to go from pixel positions to character - // positions, taking varying character widths into account. - function charFromX(line, x) { - if (x <= 0) return 0; - var lineObj = getLine(line), text = lineObj.text; - function getX(len) { - return measureLine(lineObj, len).left; - } - var from = 0, fromX = 0, to = text.length, toX; - // Guess a suitable upper bound for our search. - var estimated = Math.min(to, Math.ceil(x / charWidth())); - for (;;) { - var estX = getX(estimated); - if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); - else {toX = estX; to = estimated; break;} - } - if (x > toX) return to; - // Try to guess a suitable lower bound as well. - estimated = Math.floor(to * 0.8); estX = getX(estimated); - if (estX < x) {from = estimated; fromX = estX;} - // Do a binary search between these bounds. - for (;;) { - if (to - from <= 1) return (toX - x > x - fromX) ? from : to; - var middle = Math.ceil((from + to) / 2), middleX = getX(middle); - if (middleX > x) {to = middle; toX = middleX;} - else {from = middle; fromX = middleX;} - } - } - - var tempId = "CodeMirror-temp-" + Math.floor(Math.random() * 0xffffff).toString(16); function measureLine(line, ch) { if (ch == 0) return {top: 0, left: 0}; var wbr = options.lineWrapping && ch < line.text.length && spanAffectsWrapping.test(line.text.slice(ch - 1, ch + 1)); - measure.innerHTML = "" + line.getHTML(makeTab, ch, tempId, wbr) + ""; - var elt = document.getElementById(tempId); - var top = elt.offsetTop, left = elt.offsetLeft; + var pre = lineContent(line, ch); + removeChildrenAndAdd(measure, pre); + var anchor = pre.anchor; + var top = anchor.offsetTop, left = anchor.offsetLeft; // Older IEs report zero offsets for spans directly after a wrap if (ie && top == 0 && left == 0) { - var backup = document.createElement("span"); - backup.innerHTML = "x"; - elt.parentNode.insertBefore(backup, elt.nextSibling); + var backup = elt("span", "x"); + anchor.parentNode.insertBefore(backup, anchor.nextSibling); top = backup.offsetTop; } return {top: top, left: left}; @@ -1699,17 +1670,19 @@ var CodeMirror = (function() { } // Coords must be lineSpace-local function coordsChar(x, y) { - if (y < 0) y = 0; var th = textHeight(), cw = charWidth(), heightPos = displayOffset + Math.floor(y / th); + if (heightPos < 0) return {line: 0, ch: 0}; var lineNo = lineAtHeight(doc, heightPos); if (lineNo >= doc.size) return {line: doc.size - 1, ch: getLine(doc.size - 1).text.length}; var lineObj = getLine(lineNo), text = lineObj.text; var tw = options.lineWrapping, innerOff = tw ? heightPos - heightAtLine(doc, lineNo) : 0; if (x <= 0 && innerOff == 0) return {line: lineNo, ch: 0}; + var wrongLine = false; function getX(len) { var sp = measureLine(lineObj, len); if (tw) { var off = Math.round(sp.top / th); + wrongLine = off != innerOff; return Math.max(0, sp.left + (off - innerOff) * scroller.clientWidth); } return sp.left; @@ -1728,9 +1701,12 @@ var CodeMirror = (function() { if (estX < x) {from = estimated; fromX = estX;} // Do a binary search between these bounds. for (;;) { - if (to - from <= 1) return {line: lineNo, ch: (toX - x > x - fromX) ? from : to}; + if (to - from <= 1) { + var after = x - fromX < toX - x; + return {line: lineNo, ch: after ? from : to, after: after}; + } var middle = Math.ceil((from + to) / 2), middleX = getX(middle); - if (middleX > x) {to = middle; toX = middleX;} + if (middleX > x) {to = middle; toX = middleX; if (wrongLine) toX += 1000; } else {from = middle; fromX = middleX;} } } @@ -1739,26 +1715,32 @@ var CodeMirror = (function() { return {x: off.left + local.x, y: off.top + local.y, yBot: off.top + local.yBot}; } - var cachedHeight, cachedHeightFor, measureText; + var cachedHeight, cachedHeightFor, measurePre; function textHeight() { - if (measureText == null) { - measureText = ""; - for (var i = 0; i < 49; ++i) measureText += "x"; + if (measurePre == null) { + measurePre = elt("pre"); + for (var i = 0; i < 49; ++i) { + measurePre.appendChild(document.createTextNode("x")); + measurePre.appendChild(elt("br")); + } + measurePre.appendChild(document.createTextNode("x")); } var offsetHeight = lineDiv.clientHeight; if (offsetHeight == cachedHeightFor) return cachedHeight; cachedHeightFor = offsetHeight; - measure.innerHTML = measureText; + removeChildrenAndAdd(measure, measurePre.cloneNode(true)); cachedHeight = measure.firstChild.offsetHeight / 50 || 1; - measure.innerHTML = ""; + removeChildren(measure); return cachedHeight; } var cachedWidth, cachedWidthFor = 0; function charWidth() { if (scroller.clientWidth == cachedWidthFor) return cachedWidth; cachedWidthFor = scroller.clientWidth; - return (cachedWidth = stringWidth("x")); + var anchor = elt("span", "x"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(measure, pre); + return (cachedWidth = anchor.offsetWidth || 10); } function paddingTop() {return lineSpace.offsetTop;} function paddingLeft() {return lineSpace.offsetLeft;} @@ -1775,6 +1757,7 @@ var CodeMirror = (function() { var offL = eltOffset(lineSpace, true); return coordsChar(x - offL.left, y - offL.top); } + var detectingSelectAll; function onContextMenu(e) { var pos = posFromMouse(e), scrollPos = scrollbar.scrollTop; if (!pos || opera) return; // Opera is difficult. @@ -1786,19 +1769,30 @@ var CodeMirror = (function() { input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " + "border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; - leaveInputAlone = true; - var val = input.value = getSelection(); focusInput(); - selectInput(input); + resetInput(true); + // Adds "Select all" to context menu in FF + if (posEq(sel.from, sel.to)) input.value = prevInput = " "; + function rehide() { - var newVal = splitLines(input.value).join("\n"); - if (newVal != val && !options.readOnly) operation(replaceSelection)(newVal, "end"); inputDiv.style.position = "relative"; input.style.cssText = oldCSS; if (ie_lt9) scrollbar.scrollTop = scrollPos; - leaveInputAlone = false; - resetInput(true); slowPoll(); + + // Try to detect the user choosing select-all + if (input.selectionStart != null) { + clearTimeout(detectingSelectAll); + var extval = input.value = " " + (posEq(sel.from, sel.to) ? "" : input.value), i = 0; + prevInput = " "; + input.selectionStart = 1; input.selectionEnd = extval.length; + detectingSelectAll = setTimeout(function poll(){ + if (prevInput == " " && input.selectionStart == 0) + operation(commands.selectAll)(instance); + else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500); + else resetInput(); + }, 200); + } } if (gecko) { @@ -1819,7 +1813,7 @@ var CodeMirror = (function() { cursor.style.visibility = ""; blinker = setInterval(function() { cursor.style.visibility = (on = !on) ? "" : "hidden"; - }, 650); + }, options.cursorBlinkRate); } var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; @@ -1882,70 +1876,39 @@ var CodeMirror = (function() { return minline; } function getStateBefore(n) { - var start = findStartLine(n), state = start && getLine(start-1).stateAfter; + var pos = findStartLine(n), state = pos && getLine(pos-1).stateAfter; if (!state) state = startState(mode); else state = copyState(mode, state); - doc.iter(start, n, function(line) { - line.highlight(mode, state, options.tabSize); - line.stateAfter = copyState(mode, state); + doc.iter(pos, n, function(line) { + line.process(mode, state, options.tabSize); + line.stateAfter = (pos == n - 1 || pos % 5 == 0) ? copyState(mode, state) : null; }); - if (start < n) changes.push({from: start, to: n}); - if (n < doc.size && !getLine(n).stateAfter) work.push(n); return state; } - function highlightLines(start, end) { - var state = getStateBefore(start); - doc.iter(start, end, function(line) { - line.highlight(mode, state, options.tabSize); - line.stateAfter = copyState(mode, state); - }); - } function highlightWorker() { - var end = +new Date + options.workTime; - var foundWork = work.length; - while (work.length) { - if (!getLine(showingFrom).stateAfter) var task = showingFrom; - else var task = work.pop(); - if (task >= doc.size) continue; - var start = findStartLine(task), state = start && getLine(start-1).stateAfter; - if (state) state = copyState(mode, state); - else state = startState(mode); - - var unchanged = 0, compare = mode.compareStates, realChange = false, - i = start, bail = false; - doc.iter(i, doc.size, function(line) { - var hadState = line.stateAfter; - if (+new Date > end) { - work.push(i); - startWorker(options.workDelay); - if (realChange) changes.push({from: task, to: i + 1}); - return (bail = true); - } - var changed = line.highlight(mode, state, options.tabSize); - if (changed) realChange = true; + if (frontier >= showingTo) return; + var end = +new Date + options.workTime, state = copyState(mode, getStateBefore(frontier)); + var startFrontier = frontier; + doc.iter(frontier, showingTo, function(line) { + if (frontier >= showingFrom) { // Visible + line.highlight(mode, state, options.tabSize); line.stateAfter = copyState(mode, state); - var done = null; - if (compare) { - var same = hadState && compare(hadState, state); - if (same != Pass) done = !!same; - } - if (done == null) { - if (changed !== false || !hadState) unchanged = 0; - else if (++unchanged > 3 && (!mode.indent || mode.indent(hadState, "") == mode.indent(state, ""))) - done = true; - } - if (done) return true; - ++i; - }); - if (bail) return; - if (realChange) changes.push({from: task, to: i + 1}); - } - if (foundWork && options.onHighlightComplete) - options.onHighlightComplete(instance); + } else { + line.process(mode, state, options.tabSize); + line.stateAfter = frontier % 5 == 0 ? copyState(mode, state) : null; + } + ++frontier; + if (+new Date > end) { + startWorker(options.workDelay); + return true; + } + }); + if (showingTo > startFrontier && frontier >= showingFrom) + operation(function() {changes.push({from: startFrontier, to: frontier});})(); } function startWorker(time) { - if (!work.length) return; - highlight.set(time, operation(highlightWorker)); + if (frontier < showingTo) + highlight.set(time, highlightWorker); } // Operations are used to wrap changes in such a way that each @@ -1959,7 +1922,11 @@ var CodeMirror = (function() { function endOperation() { if (updateMaxLine) computeMaxLength(); if (maxLineChanged && !options.lineWrapping) { - widthForcer.style.left = stringWidth(maxLine) + "px"; + var cursorWidth = widthForcer.offsetWidth, left = measureLine(maxLine, maxLine.text.length).left; + if (!ie_lt8) { + widthForcer.style.left = left + "px"; + lineSpace.style.minWidth = (left + cursorWidth) + "px"; + } maxLineChanged = false; } var newScrollPos, updated; @@ -1967,16 +1934,16 @@ var CodeMirror = (function() { var coords = calculateCursorCoords(); newScrollPos = calculateScrollPos(coords.x, coords.y, coords.x, coords.yBot); } - if (changes.length) updated = updateDisplay(changes, true, (newScrollPos ? newScrollPos.scrollTop : null)); - else { + if (changes.length || newScrollPos && newScrollPos.scrollTop != null) + updated = updateDisplay(changes, true, newScrollPos && newScrollPos.scrollTop); + if (!updated) { if (selectionChanged) updateSelection(); if (gutterDirty) updateGutter(); } if (newScrollPos) scrollCursorIntoView(); - if (selectionChanged) {scrollEditorIntoView(); restartBlink();} + if (selectionChanged) restartBlink(); - if (focused && !leaveInputAlone && - (updateInput === true || (updateInput !== false && selectionChanged))) + if (focused && (updateInput === true || (updateInput !== false && selectionChanged))) resetInput(userSelChange); if (selectionChanged && options.matchBrackets) @@ -2038,17 +2005,19 @@ var CodeMirror = (function() { dragDrop: true, onChange: null, onCursorActivity: null, + onViewportChange: null, onGutterClick: null, - onHighlightComplete: null, onUpdate: null, onFocus: null, onBlur: null, onScroll: null, matchBrackets: false, + cursorBlinkRate: 530, workTime: 100, workDelay: 200, pollInterval: 100, undoDepth: 40, tabindex: null, - autofocus: null + autofocus: null, + lineNumberFormatter: function(integer) { return integer; } }; var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); @@ -2080,7 +2049,13 @@ var CodeMirror = (function() { var spec = CodeMirror.resolveMode(spec); var mfactory = modes[spec.name]; if (!mfactory) return CodeMirror.getMode(options, "text/plain"); - return mfactory(options, spec); + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) if (exts.hasOwnProperty(prop)) modeObj[prop] = exts[prop]; + } + modeObj.name = spec.name; + return modeObj; }; CodeMirror.listModes = function() { var list = []; @@ -2100,6 +2075,13 @@ var CodeMirror = (function() { extensions[name] = func; }; + var modeExtensions = CodeMirror.modeExtensions = {}; + CodeMirror.extendMode = function(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + for (var prop in properties) if (properties.hasOwnProperty(prop)) + exts[prop] = properties[prop]; + }; + var commands = CodeMirror.commands = { selectAll: function(cm) {cm.setSelection({line: 0, ch: 0}, {line: cm.lineCount() - 1});}, killLine: function(cm) { @@ -2197,6 +2179,10 @@ var CodeMirror = (function() { function lookup(map) { map = getKeyMap(map); var found = map[name]; + if (found === false) { + if (stop) stop(); + return true; + } if (found != null && handle(found)) return true; if (map.nofallthrough) { if (stop) stop(); @@ -2224,8 +2210,15 @@ var CodeMirror = (function() { options.value = textarea.value; if (!options.tabindex && textarea.tabindex) options.tabindex = textarea.tabindex; - if (options.autofocus == null && textarea.getAttribute("autofocus") != null) - options.autofocus = true; + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = document.body; + // doc.activeElement occasionally throws on IE + try { hasFocus = document.activeElement; } catch(e) {} + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } function save() {textarea.value = instance.getValue();} if (textarea.form) { @@ -2233,13 +2226,12 @@ var CodeMirror = (function() { var rmSubmit = connect(textarea.form, "submit", save, true); if (typeof textarea.form.submit == "function") { var realSubmit = textarea.form.submit; - function wrappedSubmit() { + textarea.form.submit = function wrappedSubmit() { save(); textarea.form.submit = realSubmit; textarea.form.submit(); textarea.form.submit = wrappedSubmit; - } - textarea.form.submit = wrappedSubmit; + }; } } @@ -2262,6 +2254,18 @@ var CodeMirror = (function() { return instance; }; + var gecko = /gecko\/\d{7}/i.test(navigator.userAgent); + var ie = /MSIE \d/.test(navigator.userAgent); + var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent); + var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent); + var quirksMode = ie && document.documentMode == 5; + var webkit = /WebKit\//.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 10\D([7-9]|\d\d)\D/.test(navigator.userAgent); + // Utility functions for working with state. Exported because modes // sometimes need to do this. function copyState(mode, state) { @@ -2280,6 +2284,14 @@ var CodeMirror = (function() { return mode.startState ? mode.startState(a1, a2) : true; } CodeMirror.startState = startState; + CodeMirror.innerMode = function(mode, state) { + while (mode.innerMode) { + var info = mode.innerMode(state); + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state}; + }; // The character stream used by a mode's parser. function StringStream(string, tabSize) { @@ -2290,7 +2302,7 @@ var CodeMirror = (function() { StringStream.prototype = { eol: function() {return this.pos >= this.string.length;}, sol: function() {return this.pos == 0;}, - peek: function() {return this.string.charAt(this.pos);}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, next: function() { if (this.pos < this.string.length) return this.string.charAt(this.pos++); @@ -2321,13 +2333,14 @@ var CodeMirror = (function() { indentation: function() {return countColumn(this.string, null, this.tabSize);}, match: function(pattern, consume, caseInsensitive) { if (typeof pattern == "string") { - function cased(str) {return caseInsensitive ? str.toLowerCase() : str;} + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) { if (consume !== false) this.pos += pattern.length; return true; } } else { var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; if (match && consume !== false) this.pos += match[0].length; return match; } @@ -2336,201 +2349,162 @@ var CodeMirror = (function() { }; CodeMirror.StringStream = StringStream; - function MarkedText(from, to, className, marker) { - this.from = from; this.to = to; this.style = className; this.marker = marker; + function MarkedSpan(from, to, marker) { + this.from = from; this.to = to; this.marker = marker; } - MarkedText.prototype = { - attach: function(line) { this.marker.set.push(line); }, - detach: function(line) { - var ix = indexOf(this.marker.set, line); - if (ix > -1) this.marker.set.splice(ix, 1); - }, - split: function(pos, lenBefore) { - if (this.to <= pos && this.to != null) return null; - var from = this.from < pos || this.from == null ? null : this.from - pos + lenBefore; - var to = this.to == null ? null : this.to - pos + lenBefore; - return new MarkedText(from, to, this.style, this.marker); - }, - dup: function() { return new MarkedText(null, null, this.style, this.marker); }, - clipTo: function(fromOpen, from, toOpen, to, diff) { - if (fromOpen && to > this.from && (to < this.to || this.to == null)) - this.from = null; - else if (this.from != null && this.from >= from) - this.from = Math.max(to, this.from) + diff; - if (toOpen && (from < this.to || this.to == null) && (from > this.from || this.from == null)) - this.to = null; - else if (this.to != null && this.to > from) - this.to = to < this.to ? this.to + diff : from; - }, - isDead: function() { return this.from != null && this.to != null && this.from >= this.to; }, - sameSet: function(x) { return this.marker == x.marker; } - }; - function Bookmark(pos) { - this.from = pos; this.to = pos; this.line = null; - } - Bookmark.prototype = { - attach: function(line) { this.line = line; }, - detach: function(line) { if (this.line == line) this.line = null; }, - split: function(pos, lenBefore) { - if (pos < this.from) { - this.from = this.to = (this.from - pos) + lenBefore; - return this; + function getMarkedSpanFor(spans, marker, del) { + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) { + if (del) spans.splice(i, 1); + return span; } - }, - isDead: function() { return this.from > this.to; }, - clipTo: function(fromOpen, from, toOpen, to, diff) { - if ((fromOpen || from < this.from) && (toOpen || to > this.to)) { - this.from = 0; this.to = -1; - } else if (this.from > from) { - this.from = this.to = Math.max(to, this.from) + diff; - } - }, - sameSet: function(x) { return false; }, - find: function() { - if (!this.line || !this.line.parent) return null; - return {line: lineNo(this.line), ch: this.from}; - }, - clear: function() { - if (this.line) { - var found = indexOf(this.line.marked, this); - if (found != -1) this.line.marked.splice(found, 1); - this.line = null; + } + } + + function markedSpansBefore(old, startCh, endCh) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || marker.type == "bookmark" && span.from == startCh && span.from != endCh) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); + (nw || (nw = [])).push({from: span.from, + to: endsAfter ? null : span.to, + marker: marker}); } } - }; + return nw; + } + + function markedSpansAfter(old, endCh) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || marker.type == "bookmark" && span.from == endCh) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); + (nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh, + to: span.to == null ? null : span.to - endCh, + marker: marker}); + } + } + return nw; + } - // Line objects. These hold state related to a line, including - // highlighting info (the styles array). - function Line(text, styles) { - this.styles = styles || [text, null]; - this.text = text; - this.height = 1; - this.marked = this.gutterMarker = this.className = this.bgClassName = this.handlers = null; - this.stateAfter = this.parent = this.hidden = null; - } - Line.inheritMarks = function(text, orig) { - var ln = new Line(text), mk = orig && orig.marked; - if (mk) { - for (var i = 0; i < mk.length; ++i) { - if (mk[i].to == null && mk[i].style) { - var newmk = ln.marked || (ln.marked = []), mark = mk[i]; - var nmark = mark.dup(); newmk.push(nmark); nmark.attach(ln); + function updateMarkedSpans(oldFirst, oldLast, startCh, endCh, newText) { + if (!oldFirst && !oldLast) return newText; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh); + var last = markedSpansAfter(oldLast, endCh); + + // Next, merge those two ends + var sameLine = newText.length == 1, offset = lst(newText).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) span.to = startCh; + else if (sameLine) span.to = found.to == null ? null : found.to + offset; } } } - return ln; - } - Line.prototype = { - // Replace a piece of a line, keeping the styles around it intact. - replace: function(from, to_, text) { - var st = [], mk = this.marked, to = to_ == null ? this.text.length : to_; - copyStyles(0, from, this.styles, st); - if (text) st.push(text, null); - copyStyles(to, this.text.length, this.styles, st); - this.styles = st; - this.text = this.text.slice(0, from) + text + this.text.slice(to); - this.stateAfter = null; - if (mk) { - var diff = text.length - (to - from); - for (var i = 0; i < mk.length; ++i) { - var mark = mk[i]; - mark.clipTo(from == null, from || 0, to_ == null, to, diff); - if (mark.isDead()) {mark.detach(this); mk.splice(i--, 1);} - } - } - }, - // Split a part off a line, keeping styles and markers intact. - split: function(pos, textBefore) { - var st = [textBefore, null], mk = this.marked; - copyStyles(pos, this.text.length, this.styles, st); - var taken = new Line(textBefore + this.text.slice(pos), st); - if (mk) { - for (var i = 0; i < mk.length; ++i) { - var mark = mk[i]; - var newmark = mark.split(pos, textBefore.length); - if (newmark) { - if (!taken.marked) taken.marked = []; - taken.marked.push(newmark); newmark.attach(taken); - if (newmark == mark) mk.splice(i--, 1); + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i = 0; i < last.length; ++i) { + var span = last[i]; + if (span.to != null) span.to += offset; + if (span.from == null) { + var found = getMarkedSpanFor(first, span.marker); + if (!found) { + span.from = offset; + if (sameLine) (first || (first = [])).push(span); } + } else { + span.from += offset; + if (sameLine) (first || (first = [])).push(span); } } - return taken; - }, - append: function(line) { - var mylen = this.text.length, mk = line.marked, mymk = this.marked; - this.text += line.text; - copyStyles(0, line.text.length, line.styles, this.styles); - if (mymk) { - for (var i = 0; i < mymk.length; ++i) - if (mymk[i].to == null) mymk[i].to = mylen; - } - if (mk && mk.length) { - if (!mymk) this.marked = mymk = []; - outer: for (var i = 0; i < mk.length; ++i) { - var mark = mk[i]; - if (!mark.from) { - for (var j = 0; j < mymk.length; ++j) { - var mymark = mymk[j]; - if (mymark.to == mylen && mymark.sameSet(mark)) { - mymark.to = mark.to == null ? null : mark.to + mylen; - if (mymark.isDead()) { - mymark.detach(this); - mk.splice(i--, 1); - } - continue outer; - } - } - } - mymk.push(mark); - mark.attach(this); - mark.from += mylen; - if (mark.to != null) mark.to += mylen; - } - } - }, - fixMarkEnds: function(other) { - var mk = this.marked, omk = other.marked; - if (!mk) return; - for (var i = 0; i < mk.length; ++i) { - var mark = mk[i], close = mark.to == null; - if (close && omk) { - for (var j = 0; j < omk.length; ++j) - if (omk[j].sameSet(mark)) {close = false; break;} - } - if (close) mark.to = this.text.length; - } - }, - fixMarkStarts: function() { - var mk = this.marked; - if (!mk) return; - for (var i = 0; i < mk.length; ++i) - if (mk[i].from == null) mk[i].from = 0; - }, - addMark: function(mark) { - mark.attach(this); - if (this.marked == null) this.marked = []; - this.marked.push(mark); - this.marked.sort(function(a, b){return (a.from || 0) - (b.from || 0);}); + } + + var newMarkers = [newHL(newText[0], first)]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = newText.length - 2, gapMarkers; + if (gap > 0 && first) + for (var i = 0; i < first.length; ++i) + if (first[i].to == null) + (gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker}); + for (var i = 0; i < gap; ++i) + newMarkers.push(newHL(newText[i+1], gapMarkers)); + newMarkers.push(newHL(lst(newText), last)); + } + return newMarkers; + } + + // hl stands for history-line, a data structure that can be either a + // string (line without markers) or a {text, markedSpans} object. + function hlText(val) { return typeof val == "string" ? val : val.text; } + function hlSpans(val) { return typeof val == "string" ? null : val.markedSpans; } + function newHL(text, spans) { return spans ? {text: text, markedSpans: spans} : text; } + + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) return; + for (var i = 0; i < spans.length; ++i) { + var lines = spans[i].marker.lines; + var ix = indexOf(lines, line); + lines.splice(ix, 1); + } + line.markedSpans = null; + } + + function attachMarkedSpans(line, spans) { + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + var marker = spans[i].marker.lines.push(line); + line.markedSpans = spans; + } + + // When measuring the position of the end of a line, different + // browsers require different approaches. If an empty span is added, + // many browsers report bogus offsets. Of those, some (Webkit, + // recent IE) will accept a space without moving the whole span to + // the next line when wrapping it, others work with a zero-width + // space. + var eolSpanContent = " "; + if (gecko || (ie && !ie_lt8)) eolSpanContent = "\u200b"; + else if (opera) eolSpanContent = ""; + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + function Line(text, markedSpans) { + this.text = text; + this.height = 1; + attachMarkedSpans(this, markedSpans); + } + Line.prototype = { + update: function(text, markedSpans) { + this.text = text; + this.stateAfter = this.styles = null; + detachMarkedSpans(this); + attachMarkedSpans(this, markedSpans); }, // Run the given mode's parser over a line, update the styles // array, which contains alternating fragments of text and CSS // classes. highlight: function(mode, state, tabSize) { - var stream = new StringStream(this.text, tabSize), st = this.styles, pos = 0; - var changed = false, curWord = st[0], prevWord; + var stream = new StringStream(this.text, tabSize), st = this.styles || (this.styles = []); + var pos = st.length = 0; if (this.text == "" && mode.blankLine) mode.blankLine(state); while (!stream.eol()) { - var style = mode.token(stream, state); - var substr = this.text.slice(stream.start, stream.pos); + var style = mode.token(stream, state), substr = stream.current(); stream.start = stream.pos; - if (pos && st[pos-1] == style) + if (pos && st[pos-1] == style) { st[pos-2] += substr; - else if (substr) { - if (!changed && (st[pos+1] != style || (pos && st[pos-2] != prevWord))) changed = true; + } else if (substr) { st[pos++] = substr; st[pos++] = style; - prevWord = curWord; curWord = st[pos]; } // Give up when line is ridiculously long if (stream.pos > 5000) { @@ -2538,17 +2512,19 @@ var CodeMirror = (function() { break; } } - if (st.length != pos) {st.length = pos; changed = true;} - if (pos && st[pos-2] != prevWord) changed = true; - // Short lines with simple highlights return null, and are - // counted as changed by the driver because they are likely to - // highlight the same way in various contexts. - return changed || (st.length < 5 && this.text.length < 10 ? null : false); + }, + process: function(mode, state, tabSize) { + var stream = new StringStream(this.text, tabSize); + if (this.text == "" && mode.blankLine) mode.blankLine(state); + while (!stream.eol() && stream.pos <= 5000) { + mode.token(stream, state); + stream.start = stream.pos; + } }, // Fetch the parser token for a given character. Useful for hacks // that want to inspect the mode state (say, for completion). - getTokenAt: function(mode, state, ch) { - var txt = this.text, stream = new StringStream(txt); + getTokenAt: function(mode, state, tabSize, ch) { + var txt = this.text, stream = new StringStream(txt, tabSize); while (stream.pos < ch && !stream.eol()) { stream.start = stream.pos; var style = mode.token(stream, state); @@ -2562,98 +2538,108 @@ var CodeMirror = (function() { indentation: function(tabSize) {return countColumn(this.text, null, tabSize);}, // Produces an HTML fragment for the line, taking selection, // marking, and highlighting into account. - getHTML: function(makeTab, wrapAt, wrapId, wrapWBR) { - var html = [], first = true, col = 0; - function span_(text, style) { + getContent: function(tabSize, wrapAt, compensateForWrapping) { + var first = true, col = 0, specials = /[\t\u0000-\u0019\u200b\u2028\u2029\uFEFF]/g; + var pre = elt("pre"); + function span_(html, text, style) { if (!text) return; // Work around a bug where, in some compat modes, IE ignores leading spaces if (first && ie && text.charAt(0) == " ") text = "\u00a0" + text.slice(1); first = false; - if (text.indexOf("\t") == -1) { + if (!specials.test(text)) { col += text.length; - var escaped = htmlEscape(text); + var content = document.createTextNode(text); } else { - var escaped = ""; - for (var pos = 0;;) { - var idx = text.indexOf("\t", pos); - if (idx == -1) { - escaped += htmlEscape(text.slice(pos)); - col += text.length - pos; - break; + var content = document.createDocumentFragment(), pos = 0; + while (true) { + specials.lastIndex = pos; + var m = specials.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + content.appendChild(document.createTextNode(text.slice(pos, pos + skipped))); + col += skipped; + } + if (!m) break; + pos += skipped + 1; + if (m[0] == "\t") { + var tabWidth = tabSize - col % tabSize; + content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + col += tabWidth; } else { - col += idx - pos; - var tab = makeTab(col); - escaped += htmlEscape(text.slice(pos, idx)) + tab.html; - col += tab.width; - pos = idx + 1; + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + m[0].charCodeAt(0).toString(16); + content.appendChild(token); + col += 1; } } } - if (style) html.push('', escaped, ""); - else html.push(escaped); + if (style) html.appendChild(elt("span", [content], style)); + else html.appendChild(content); } var span = span_; if (wrapAt != null) { - var outPos = 0, open = ""; - span = function(text, style) { + var outPos = 0, anchor = pre.anchor = elt("span"); + span = function(html, text, style) { var l = text.length; if (wrapAt >= outPos && wrapAt < outPos + l) { if (wrapAt > outPos) { - span_(text.slice(0, wrapAt - outPos), style); + span_(html, text.slice(0, wrapAt - outPos), style); // See comment at the definition of spanAffectsWrapping - if (wrapWBR) html.push("
"; - measureText += "x"); + if (compensateForWrapping) html.appendChild(elt("wbr")); } - html.push(open); + html.appendChild(anchor); var cut = wrapAt - outPos; - span_(opera ? text.slice(cut, cut + 1) : text.slice(cut), style); - html.push(" "); - if (opera) span_(text.slice(cut + 1), style); + span_(anchor, opera ? text.slice(cut, cut + 1) : text.slice(cut), style); + if (opera) span_(html, text.slice(cut + 1), style); wrapAt--; outPos += l; } else { outPos += l; - span_(text, style); - // Output empty wrapper when at end of line - if (outPos == wrapAt && outPos == len) html.push(open + " "); + span_(html, text, style); + if (outPos == wrapAt && outPos == len) { + setTextContent(anchor, eolSpanContent); + html.appendChild(anchor); + } // Stop outputting HTML when gone sufficiently far beyond measure else if (outPos > wrapAt + 10 && /\s/.test(text)) span = function(){}; } - } + }; } - var st = this.styles, allText = this.text, marked = this.marked; + var st = this.styles, allText = this.text, marked = this.markedSpans; var len = allText.length; function styleToClass(style) { if (!style) return null; return "cm-" + style.replace(/ +/g, " cm-"); } - if (!allText && wrapAt == null) { - span(" "); + span(pre, " "); } else if (!marked || !marked.length) { for (var i = 0, ch = 0; ch < len; i+=2) { var str = st[i], style = st[i+1], l = str.length; if (ch + l > len) str = str.slice(0, len - ch); ch += l; - span(str, styleToClass(style)); + span(pre, str, styleToClass(style)); } } else { + marked.sort(function(a, b) { return a.from - b.from; }); var pos = 0, i = 0, text = "", style, sg = 0; var nextChange = marked[0].from || 0, marks = [], markpos = 0; - function advanceMarks() { + var advanceMarks = function() { var m; while (markpos < marked.length && ((m = marked[markpos]).from == pos || m.from == null)) { - if (m.style != null) marks.push(m); + if (m.marker.type == "range") marks.push(m); ++markpos; } nextChange = markpos < marked.length ? marked[markpos].from : Infinity; for (var i = 0; i < marks.length; ++i) { - var to = marks[i].to || Infinity; + var to = marks[i].to; + if (to == null) to = Infinity; if (to == pos) marks.splice(i--, 1); else nextChange = Math.min(to, nextChange); } - } + }; var m = 0; while (pos < len) { if (nextChange == pos) advanceMarks(); @@ -2662,9 +2648,13 @@ var CodeMirror = (function() { if (text) { var end = pos + text.length; var appliedStyle = style; - for (var j = 0; j < marks.length; ++j) - appliedStyle = (appliedStyle ? appliedStyle + " " : "") + marks[j].style; - span(end > upto ? text.slice(0, upto - pos) : text, appliedStyle); + for (var j = 0; j < marks.length; ++j) { + var mark = marks[j]; + appliedStyle = (appliedStyle ? appliedStyle + " " : "") + mark.marker.style; + if (mark.marker.endStyle && mark.to === Math.min(end, upto)) appliedStyle += " " + mark.marker.endStyle; + if (mark.marker.startStyle && mark.from === pos) appliedStyle += " " + mark.marker.startStyle; + } + span(pre, end > upto ? text.slice(0, upto - pos) : text, appliedStyle); if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} pos = end; } @@ -2672,28 +2662,13 @@ var CodeMirror = (function() { } } } - return html.join(""); + return pre; }, cleanUp: function() { this.parent = null; - if (this.marked) - for (var i = 0, e = this.marked.length; i < e; ++i) this.marked[i].detach(this); + detachMarkedSpans(this); } }; - // Utility used by replace and split above - function copyStyles(from, to, source, dest) { - for (var i = 0, pos = 0, state = 0; pos < to; i+=2) { - var part = source[i], end = pos + part.length; - if (state == 0) { - if (end > from) dest.push(part.slice(from - pos, Math.min(part.length, to - pos)), source[i+1]); - if (end >= from) state = 1; - } else if (state == 1) { - if (end > to) dest.push(part.slice(0, to - pos), source[i+1]); - else dest.push(part, source[i+1]); - } - pos = end; - } - } // Data structure that holds the sequence of lines. function LeafChunk(lines) { @@ -2894,7 +2869,7 @@ var CodeMirror = (function() { History.prototype = { addChange: function(start, added, old) { this.undone.length = 0; - var time = +new Date, cur = this.done[this.done.length - 1], last = cur && cur[cur.length - 1]; + var time = +new Date, cur = lst(this.done), last = cur && lst(cur); var dtime = time - this.time; if (this.compound && cur && !this.closed) { @@ -2943,10 +2918,14 @@ var CodeMirror = (function() { function e_target(e) {return e.target || e.srcElement;} function e_button(e) { - if (e.which) return e.which; - else if (e.button & 1) return 1; - else if (e.button & 2) return 3; - else if (e.button & 4) return 2; + var b = e.which; + if (b == null) { + if (e.button & 1) b = 1; + else if (e.button & 2) b = 3; + else if (e.button & 4) b = 2; + } + if (mac && e.ctrlKey && b == 1) b = 3; + return b; } // Allow 3rd-party code to override event properties by adding an override @@ -2975,30 +2954,18 @@ var CodeMirror = (function() { var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; - var gecko = /gecko\/\d{7}/i.test(navigator.userAgent); - var ie = /MSIE \d/.test(navigator.userAgent); - var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent); - var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent); - var quirksMode = ie && document.documentMode == 5; - var webkit = /WebKit\//.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 10\D([7-9]|\d\d)\D/.test(navigator.userAgent); - // Detect drag-and-drop var dragAndDrop = function() { // There is *some* kind of drag-and-drop support in IE6-8, but I // couldn't get it to work yet. if (ie_lt9) return false; - var div = document.createElement('div'); + var div = elt('div'); return "draggable" in div || "dragDrop" in div; }(); // Feature-detect whether newlines in textareas are converted to \r\n var lineSep = function () { - var te = document.createElement("textarea"); + var te = elt("textarea"); te.value = "foo\nbar"; if (te.value.indexOf("\r") > -1) return "\r\n"; return "\n"; @@ -3030,31 +2997,7 @@ var CodeMirror = (function() { return n; } - function computedStyle(elt) { - if (elt.currentStyle) return elt.currentStyle; - return window.getComputedStyle(elt, null); - } - - // Find the position of an element by following the offsetParent chain. - // If screen==true, it returns screen (rather than page) coordinates. function eltOffset(node, screen) { - var bod = node.ownerDocument.body; - var x = 0, y = 0, skipBody = false; - for (var n = node; n; n = n.offsetParent) { - var ol = n.offsetLeft, ot = n.offsetTop; - // Firefox reports weird inverted offsets when the body has a border. - if (n == bod) { x += Math.abs(ol); y += Math.abs(ot); } - else { x += ol, y += ot; } - if (screen && computedStyle(n).position == "fixed") - skipBody = true; - } - var e = screen && !skipBody ? null : bod; - for (var n = node.parentNode; n != e; n = n.parentNode) - if (n.scrollLeft != null) { x -= n.scrollLeft; y -= n.scrollTop;} - return {left: x, top: y}; - } - // Use the faster and saner getBoundingClientRect method when possible. - if (document.documentElement.getBoundingClientRect != null) eltOffset = function(node, screen) { // Take the parts of bounding client rect that we are interested in so we are able to edit if need be, // since the returned value cannot be changed externally (they are kept in sync as the element moves within the page) try { var box = node.getBoundingClientRect(); box = { top: box.top, left: box.left }; } @@ -3070,12 +3013,21 @@ var CodeMirror = (function() { } } return box; - }; + } - // Get a node's text content. function eltText(node) { return node.textContent || node.innerText || node.nodeValue || ""; } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + spaceStrs.push(lst(spaceStrs) + " "); + return spaceStrs[n]; + } + + function lst(arr) { return arr[arr.length-1]; } + function selectInput(node) { if (ios) { // Mobile Safari apparently has a bug where select() is broken. node.selectionStart = 0; @@ -3088,27 +3040,27 @@ var CodeMirror = (function() { function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);} function copyPos(x) {return {line: x.line, ch: x.ch};} - var escapeElement = document.createElement("pre"); - function htmlEscape(str) { - escapeElement.textContent = str; - return escapeElement.innerHTML; + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") setTextContent(e, content); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; } - // Recent (late 2011) Opera betas insert bogus newlines at the start - // of the textContent, so we strip those. - if (htmlEscape("a") == "\na") { - htmlEscape = function(str) { - escapeElement.textContent = str; - return escapeElement.innerHTML.slice(1); - }; - // Some IEs don't preserve tabs through innerHTML - } else if (htmlEscape("\t") != "\t") { - htmlEscape = function(str) { - escapeElement.innerHTML = ""; - escapeElement.appendChild(document.createTextNode(str)); - return escapeElement.innerHTML; - }; + function removeChildren(e) { + e.innerHTML = ""; + return e; + } + function removeChildrenAndAdd(parent, e) { + removeChildren(parent).appendChild(e); } - CodeMirror.htmlEscape = htmlEscape; + function setTextContent(e, str) { + if (ie_lt9) { + e.innerHTML = ""; + e.appendChild(document.createTextNode(str)); + } else e.textContent = str; + } // Used to position the cursor after an undo/redo by finding the // last edited character. @@ -3133,14 +3085,22 @@ var CodeMirror = (function() { // See if "".split is the broken IE version, if so, provide an // alternative way to split lines. var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { - var pos = 0, nl, result = []; - while ((nl = string.indexOf("\n", pos)) > -1) { - result.push(string.slice(pos, string.charAt(nl-1) == "\r" ? nl - 1 : nl)); - pos = nl + 1; + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) nl = string.length; + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } } - result.push(string.slice(pos)); return result; - } : function(string){return string.split(/\r?\n/);}; + } : function(string){return string.split(/\r\n?|\n/);}; CodeMirror.splitLines = splitLines; var hasSelection = window.getSelection ? function(te) { @@ -3175,5 +3135,7 @@ var CodeMirror = (function() { for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; })(); + CodeMirror.version = "2.34"; + return CodeMirror; })();