From 6ac8c130e589952e74c72a1840abc6e2358550b3 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 15 Aug 2012 13:19:43 +0200 Subject: [PATCH] Implement arbitrary-height lines Experimental for now. --- lib/codemirror.css | 2 +- lib/codemirror.js | 223 +++++++++++++++++++++++++-------------------- test/index.html | 1 + test/test.js | 17 ++-- 4 files changed, 136 insertions(+), 107 deletions(-) diff --git a/lib/codemirror.css b/lib/codemirror.css index ddd3ac04ac..623c799885 100644 --- a/lib/codemirror.css +++ b/lib/codemirror.css @@ -1,5 +1,5 @@ .CodeMirror { - line-height: 1em; + line-height: 1; font-family: monospace; /* Necessary so the scrollbar can be absolutely positioned within the wrapper on Lion. */ diff --git a/lib/codemirror.js b/lib/codemirror.js index 227b78b090..5408a0bf8b 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -66,7 +66,7 @@ window.CodeMirror = (function() { // mode holds a mode API object. doc is the tree of Line objects, // work an array of lines that should be parsed, and history the // undo history (instance of History constructor). - var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), work, focused; + var mode, doc = new BranchChunk([new LeafChunk([new Line("", null, textHeight())])]), work, focused; loadMode(); // The selection. These are always maintained to point at valid // positions. Inverted is used to remember that the user is @@ -244,7 +244,7 @@ window.CodeMirror = (function() { sizer.appendChild(node); if (vert == "over") top = pos.y; else if (vert == "near") { - var vspace = Math.max(scroller.offsetHeight, doc.height * textHeight()), + var vspace = Math.max(scroller.offsetHeight, doc.height), hspace = Math.max(sizer.clientWidth, lineSpace.clientWidth); if (pos.yBot + node.offsetHeight > vspace && pos.y > node.offsetHeight) top = pos.y - node.offsetHeight; @@ -745,6 +745,7 @@ window.CodeMirror = (function() { }); var nlines = to.line - from.line, firstLine = getLine(from.line), lastLine = getLine(to.line); + var th = textHeight(); // First adjust the line structure, taking some care to leave highlighting intact. if (from.ch == 0 && to.ch == 0 && newText[newText.length - 1] == "") { // This is a whole-line replace. Treated specially to make @@ -755,7 +756,7 @@ window.CodeMirror = (function() { prevLine.fixMarkEnds(lastLine); } else lastLine.fixMarkStarts(); for (var i = 0, e = newText.length - 1; i < e; ++i) - added.push(Line.inheritMarks(newText[i], prevLine)); + added.push(Line.inheritMarks(newText[i], prevLine, th)); if (nlines) doc.remove(from.line, nlines, callbacks); if (added.length) doc.insert(from.line, added); } else if (firstLine == lastLine) { @@ -767,7 +768,7 @@ window.CodeMirror = (function() { firstLine.fixMarkEnds(lastLine); var added = []; for (var i = 1, e = newText.length - 1; i < e; ++i) - added.push(Line.inheritMarks(newText[i], firstLine)); + added.push(Line.inheritMarks(newText[i], firstLine, th)); added.push(lastLine); doc.insert(from.line + 1, added); } @@ -782,7 +783,7 @@ window.CodeMirror = (function() { lastLine.replace(null, to.ch, newText[newText.length-1]); firstLine.fixMarkEnds(lastLine); for (var i = 1, e = newText.length - 1; i < e; ++i) - added.push(Line.inheritMarks(newText[i], firstLine)); + added.push(Line.inheritMarks(newText[i], firstLine, th)); if (nlines > 1) doc.remove(from.line + 1, nlines - 1, callbacks); doc.insert(from.line + 1, added); } @@ -790,7 +791,7 @@ window.CodeMirror = (function() { var perLine = Math.max(5, scroller.clientWidth / charWidth() - 3); doc.iter(from.line, from.line + newText.length, function(line) { if (line.hidden) return; - var guess = Math.ceil(line.text.length / perLine) || 1; + var guess = (Math.ceil(line.text.length / perLine) || 1) * th; if (guess != line.height) updateLineHeight(line, guess); }); } else { @@ -832,8 +833,8 @@ window.CodeMirror = (function() { } function needsScrollbar() { - var realHeight = doc.height * textHeight() + 2 * paddingTop(); - return realHeight * .99 > scroller.offsetHeight ? realHeight : false; + var realHeight = doc.height + 2 * paddingTop(); + return realHeight - 1 > scroller.offsetHeight ? realHeight : false; } function updateVerticalScroll(scrollTop) { @@ -861,7 +862,7 @@ window.CodeMirror = (function() { sizer.style.minHeight = scroller.clientHeight + "px"; } // Position the mover div to align with the current virtual scroll position - mover.style.top = displayOffset * textHeight() + "px"; + mover.style.top = displayOffset + "px"; } function computeMaxLength() { @@ -1027,9 +1028,9 @@ window.CodeMirror = (function() { } function visibleLines(scrollTop) { - var lh = textHeight(), top = (scrollTop != null ? scrollTop : scrollbar.scrollTop) - paddingTop(); - var fromHeight = Math.max(0, Math.floor(top / lh)); - var toHeight = Math.ceil((top + scroller.clientHeight) / lh); + var top = (scrollTop != null ? scrollTop : scrollbar.scrollTop) - paddingTop(); + var fromHeight = Math.max(0, Math.floor(top)); + var toHeight = Math.ceil(top + scroller.clientHeight); return {from: lineAtHeight(doc, fromHeight), to: lineAtHeight(doc, toHeight)}; } @@ -1082,15 +1083,14 @@ window.CodeMirror = (function() { } intact.sort(function(a, b) {return a.domStart - b.domStart;}); - var th = textHeight(); lineDiv.style.display = "none"; patchDisplay(from, to, intact, positionsChangedFrom); lineDiv.style.display = ""; - var different = from != showingFrom || to != showingTo || lastSizeC != scroller.clientHeight + th; + var different = from != showingFrom || to != showingTo || lastSizeC != scroller.clientHeight; // This is just a bogus formula that detects when the editor is // resized or the font size changes. - if (different) lastSizeC = scroller.clientHeight + th; + if (different) lastSizeC = scroller.clientHeight; if (from != showingFrom || to != showingTo && options.onViewportChange) setTimeout(function(){ if (options.onViewportChange) options.onViewportChange(instance, from, to); @@ -1104,26 +1104,27 @@ window.CodeMirror = (function() { throw new Error("BAD PATCH! " + JSON.stringify(intact) + " size=" + (showingTo - showingFrom) + " nodes=" + lineDiv.childNodes.length); - function checkHeights() { - var curNode = lineDiv.firstChild, heightChanged = false; - doc.iter(showingFrom, showingTo, function(line) { - // Work around bizarro IE7 bug where, sometimes, our curNode - // is magically replaced with a new node in the DOM, leaving - // us with a reference to an orphan (nextSibling-less) node. - if (!curNode) return; - if (!line.hidden) { - var height = Math.round(curNode.offsetHeight / th) || 1; - if (line.height != height) { - updateLineHeight(line, height); - heightChanged = true; - } + // Update line heights for visible lines based on actual DOM + // sizes + var curNode = lineDiv.firstChild, heightChanged = false; + var relativeTo = curNode.offsetTop; + doc.iter(showingFrom, showingTo, function(line) { + // Work around bizarro IE7 bug where, sometimes, our curNode + // is magically replaced with a new node in the DOM, leaving + // us with a reference to an orphan (nextSibling-less) node. + if (!curNode) return; + if (!line.hidden) { + var end = curNode.offsetHeight + curNode.offsetTop; + var height = end - relativeTo, diff = line.height - height; + if (height < 2) height = textHeight(); + relativeTo = end; + if (diff > .001 || diff < -.001) { + updateLineHeight(line, height); + heightChanged = true; } - curNode = curNode.nextSibling; - }); - return heightChanged; - } - - if (options.lineWrapping) checkHeights(); + } + curNode = curNode.nextSibling; + }); updateVerticalScroll(scrollTop); updateSelection(); @@ -1269,35 +1270,36 @@ window.CodeMirror = (function() { var collapsed = posEq(sel.from, sel.to); var fromPos = localCoords(sel.from, true); var toPos = collapsed ? fromPos : localCoords(sel.to, true); - var headPos = sel.inverted ? fromPos : toPos, th = textHeight(); + var headPos = sel.inverted ? fromPos : toPos; var wrapOff = eltOffset(wrapper), lineOff = eltOffset(lineDiv); inputDiv.style.top = Math.max(0, Math.min(scroller.offsetHeight, headPos.y + lineOff.top - wrapOff.top)) + "px"; inputDiv.style.left = Math.max(0, Math.min(scroller.offsetWidth, headPos.x + lineOff.left - wrapOff.left)) + "px"; if (collapsed) { cursor.style.top = headPos.y + "px"; cursor.style.left = (options.lineWrapping ? Math.min(headPos.x, lineSpace.offsetWidth) : headPos.x) + "px"; + cursor.style.height = (headPos.yBot - headPos.y) * .85 + "px"; cursor.style.display = ""; selectionDiv.style.display = "none"; } else { + fromPos.y = Math.max(0, fromPos.y); toPos.y = Math.max(0, toPos.y); var sameLine = fromPos.y == toPos.y, fragment = document.createDocumentFragment(); var clientWidth = lineSpace.clientWidth || lineSpace.offsetWidth; - var clientHeight = lineSpace.clientHeight || lineSpace.offsetHeight; var add = function(left, top, right, height) { var rstyle = quirksMode ? "width: " + (!right ? clientWidth : clientWidth - right - left) + "px" : "right: " + right + "px"; 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) { + if (sel.from.ch) { var right = sameLine ? clientWidth - toPos.x : 0; - add(fromPos.x, fromPos.y, right, th); + add(fromPos.x, fromPos.y, right, fromPos.yBot - fromPos.y); } - var middleStart = Math.max(0, fromPos.y + (sel.from.ch ? th : 0)); - var middleHeight = Math.min(toPos.y, clientHeight) - middleStart; - if (middleHeight > 0.2 * th) + var middleStart = Math.max(0, sel.from.ch ? fromPos.yBot - .5 : fromPos.y); + var middleHeight = toPos.y + (sel.from.ch ? 1 : 0) - middleStart; + if (middleHeight > 2) add(paddingLeft(), middleStart, 0, middleHeight); - if ((!sameLine || !sel.from.ch) && sel.to.ch && toPos.y < clientHeight - .5 * th) - add(paddingLeft(), toPos.y, clientWidth - toPos.x, th); + if ((!sameLine || !sel.from.ch) && sel.to.ch) + add(paddingLeft(), toPos.y, clientWidth - toPos.x, toPos.yBot - toPos.y); removeChildrenAndAdd(selectionDiv, fragment); cursor.style.display = "none"; selectionDiv.style.display = ""; @@ -1432,17 +1434,18 @@ window.CodeMirror = (function() { } function moveV(dir, unit) { var dist = 0, pos = localCoords(sel.inverted ? sel.from : sel.to, true); - if (goalColumn != null) pos.x = goalColumn; + var x = pos.x, y; + if (goalColumn != null) x = goalColumn; if (unit == "page") { - var screen = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight); - var target = coordsChar(pos.x, pos.y + screen * dir); + var pageSize = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight); + y = pos.y + dir * pageSize; } else if (unit == "line") { - var th = textHeight(); - var target = coordsChar(pos.x, pos.y + .5 * th + dir * th); + y = dir > 0 ? pos.yBot + 3 : pos.y - 3; } + var target = coordsChar(x, y); if (unit == "page") scrollbar.scrollTop += localCoords(target, true).y - pos.y; setCursor(target.line, target.ch, true); - goalColumn = pos.x; + goalColumn = x; } function findWordAt(pos) { @@ -1701,51 +1704,73 @@ window.CodeMirror = (function() { } function measureLine(line, ch) { - if (ch == 0) return {top: 0, left: paddingLeft()}; - - var wbr = options.lineWrapping && ch < line.text.length && - spanAffectsWrapping.test(line.text.slice(ch - 1, ch + 1)); - var pre = line.getElement(makeTab, ch, wbr); + var pre = line.getElement(makeTab, ch, options.lineWrapping); removeChildrenAndAdd(measure, pre); - var anchor = pre.anchor; + var anchor = pre.anchor, left = anchor.offsetLeft; + // We'll sample once at the top, once at the bottom of the line, + // to get the real line height (in case there tokens on the line + // with bigger fonts) + anchor.style.verticalAlign = "top"; + if (ie) { + // In IE, verticalAlign does not influence offsetTop, unless + // the element is an inline-block. Unfortunately, inline + // blocks have different wrapping behaviour, so we have to do + // some icky thing with inserting "Zero-Width No-Break Spaces" + // to compensate for wrapping artifacts. + anchor.style.display = "inline-block"; + if (options.lineWrapping && anchor.offsetLeft != left) { + anchor.parentNode.insertBefore(document.createTextNode("\ufeff"), anchor); + if (anchor.offsetLeft != left) + anchor.parentNode.insertBefore(document.createTextNode("\ufeff"), anchor.nextSibling); + if (anchor.offsetLeft != left) + anchor.parentNode.removeChild(anchor.previousSibling); + } + } 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 = elt("span", "x"); - anchor.parentNode.insertBefore(backup, anchor.nextSibling); - top = backup.offsetTop; + anchor = anchor.parentNode.insertBefore(elt("span", "x"), anchor.nextSibling); + top = anchor.offsetTop; left = anchor.offsetLeft; } - return {top: top, left: left}; + anchor.style.verticalAlign = "bottom"; + var bottom = anchor.offsetTop + anchor.offsetHeight; + // Work around Opera issue -- we can't get reliable wrapping + // info if we put anything in the anchor, but we can't get a + // proper height on empty nodes. + if (!eolSpanContent && bottom == top && ch == line.text.length) { + setTextContent(anchor, " "); + bottom = anchor.offsetTop + anchor.offsetHeight * (left == anchor.offsetLeft ? 1 : .5); + } + return {top: top, bottom: bottom, left: left}; } function localCoords(pos, inLineWrap) { - var x, lh = textHeight(), y = lh * (heightAtLine(doc, pos.line) - (inLineWrap ? displayOffset : 0)); + var x, y = heightAtLine(doc, pos.line) - (inLineWrap ? displayOffset : 0); var sp = measureLine(getLine(pos.line), pos.ch); - x = sp.left; - if (options.lineWrapping) y += Math.max(0, sp.top); - return {x: x, y: y, yBot: y + lh}; + return {x: sp.left, y: y + sp.top, yBot: y + sp.bottom}; } // Coords must be lineSpace-local function coordsChar(x, y) { - var th = textHeight(), cw = charWidth(), heightPos = displayOffset + Math.floor(y / th); + var cw = charWidth(), heightPos = displayOffset + y; 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}; + if (x < 0) x = 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); + wrongLine = true; + if (innerOff > sp.bottom) return Math.max(0, sp.left - scroller.clientWidth); + else if (innerOff < sp.top) return sp.left + scroller.clientWidth; + else wrongLine = false; } return sp.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 + innerOff * scroller.clientWidth * .9) / cw)); + var estimated = Math.min(to, Math.ceil((x + Math.floor(innerOff / textHeight()) * scroller.clientWidth * .9) / cw)); for (;;) { var estX = getX(estimated); if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); @@ -2478,13 +2503,13 @@ window.CodeMirror = (function() { // Line objects. These hold state related to a line, including // highlighting info (the styles array). - function Line(text, styles) { + function Line(text, styles, height) { this.styles = styles || [text, null]; this.text = text; - this.height = 1; + this.height = height; } - Line.inheritMarks = function(text, orig) { - var ln = new Line(text), mk = orig && orig.marked; + Line.inheritMarks = function(text, orig, height) { + var ln = new Line(text, null, height), mk = orig && orig.marked; if (mk) { for (var i = 0; i < mk.length; ++i) { if (mk[i].to == null && mk[i].style) { @@ -2518,7 +2543,7 @@ window.CodeMirror = (function() { 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); + var taken = new Line(textBefore + this.text.slice(pos), st, this.height); if (mk) { for (var i = 0; i < mk.length; ++i) { var mark = mk[i]; @@ -2645,10 +2670,10 @@ window.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. - getElement: function(makeTab, wrapAt, wrapWBR) { + getElement: function(makeTab, 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) { + function span_(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); @@ -2680,35 +2705,37 @@ window.CodeMirror = (function() { } } } - if (style) html.appendChild(elt("span", [content], style)); - else html.appendChild(content); + if (style != null) return pre.appendChild(elt("span", [content], style)); + else return pre.appendChild(content); } var span = span_; if (wrapAt != null) { - var outPos = 0, anchor = pre.anchor = elt("span"); - span = function(html, text, style) { + var outPos = 0; + span = function(text, style) { var l = text.length; if (wrapAt >= outPos && wrapAt < outPos + l) { + var cut = wrapAt - outPos; if (wrapAt > outPos) { - span_(html, text.slice(0, wrapAt - outPos), style); + span_(text.slice(0, cut), style); // See comment at the definition of spanAffectsWrapping - if (wrapWBR) html.appendChild(elt("wbr")); + if (compensateForWrapping && spanAffectsWrapping.test(text.slice(cut - 1, cut + 1))) + pre.appendChild(elt("wbr")); + } + if (cut + 1 == l) { + pre.anchor = span_(text.slice(cut), style || ""); + wrapAt--; + } else { + pre.anchor = span_(text.slice(cut, cut + 1), style || ""); + if (compensateForWrapping && spanAffectsWrapping.test(text.slice(cut, cut + 2))) + pre.appendChild(elt("wbr")); + span_(text.slice(cut + 1), style); } - html.appendChild(anchor); - var cut = wrapAt - outPos; - 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_(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(){}; + span_(text, style); + if (outPos == wrapAt && outPos == len) + pre.anchor = pre.appendChild(elt("span", eolSpanContent)); } }; } @@ -2719,14 +2746,14 @@ window.CodeMirror = (function() { if (!style) return null; return "cm-" + style.replace(/ +/g, " cm-"); } - if (!allText && wrapAt == null) { - span(pre, " "); + if (!allText) { + span("\u00a0"); } 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(pre, str, styleToClass(style)); + span(str, styleToClass(style)); } } else { var pos = 0, i = 0, text = "", style, sg = 0; @@ -2756,7 +2783,7 @@ window.CodeMirror = (function() { var appliedStyle = style; for (var j = 0; j < marks.length; ++j) appliedStyle = (appliedStyle ? appliedStyle + " " : "") + marks[j].style; - span(pre, end > upto ? text.slice(0, upto - pos) : text, appliedStyle); + span(end > upto ? text.slice(0, upto - pos) : text, appliedStyle); if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} pos = end; } diff --git a/test/index.html b/test/index.html index dd319e9db7..13aba5042f 100644 --- a/test/index.html +++ b/test/index.html @@ -1,6 +1,7 @@ + CodeMirror: Test Suite diff --git a/test/test.js b/test/test.js index 4ba5d5de71..099f8a934a 100644 --- a/test/test.js +++ b/test/test.js @@ -23,6 +23,7 @@ function byClassName(elt, cls) { } var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent); +var phantom = /PhantomJS/.test(navigator.userAgent); test("fromTextArea", function() { var te = document.getElementById("code"); @@ -168,9 +169,8 @@ testCM("coordsChar", function(cm) { for (var line = 0; line < 70; line += 5) { cm.setCursor(line, ch); var coords = cm.charCoords({line: line, ch: ch}); - var pos = cm.coordsChar({x: coords.x, y: coords.y + 1}); - eq(pos.line, line); - eq(pos.ch, ch); + var pos = cm.coordsChar({x: coords.x, y: coords.y + 5}); + eqPos(pos, {line: line, ch: ch}); } } }); @@ -341,7 +341,7 @@ testCM("selectionPos", function(cm) { addDoc(cm, 200, 100); cm.setSelection({line: 1, ch: 100}, {line: 98, ch: 100}); var lineWidth = cm.charCoords({line: 0, ch: 200}, "local").x; - var lineHeight = cm.charCoords({line: 1}).y - cm.charCoords({line: 0}).y; + var lineHeight = (cm.charCoords({line: 99}).y - cm.charCoords({line: 0}).y) / 100; cm.scrollTo(0, 0); var selElt = byClassName(cm.getWrapperElement(), "CodeMirror-selected"); var outer = cm.getWrapperElement().getBoundingClientRect(); @@ -353,7 +353,7 @@ testCM("selectionPos", function(cm) { var atRight = box.right - outer.left > .8 * lineWidth; if (atLeft && atRight) { sawMiddle = true; - is(box.bottom - box.top > 95 * lineHeight, "middle high"); + is(box.bottom - box.top > 90 * lineHeight, "middle high"); is(width > .9 * lineWidth, "middle wide"); } else { is(width > .4 * lineWidth, "top/bot wide enough"); @@ -473,7 +473,7 @@ testCM("wrappingAndResizing", function(cm) { {line: 0, ch: 0}, {line: 1, ch: doc.length}, {line: 1, ch: doc.length - 1}], function(pos) { var coords = cm.charCoords(pos); - eqPos(pos, cm.coordsChar({x: coords.x + 2, y: coords.y + 2})); + eqPos(pos, cm.coordsChar({x: coords.x + 2, y: coords.y + 5})); }); }, null, ie_lt8); @@ -519,7 +519,7 @@ testCM("moveV stuck", function(cm) { cm.setCursor({line: 0, ch: val.length - 1}); cm.moveV(-1, "line"); eqPos(cm.getCursor(), {line: 0, ch: 26}); -}, {lineWrapping: true}, ie_lt8); +}, {lineWrapping: true}); testCM("clickTab", function(cm) { var p0 = cm.charCoords({line: 0, ch: 0}), p1 = cm.charCoords({line: 0, ch: 1}); @@ -624,7 +624,8 @@ testCM("verticalMovementCommands", function(cm) { cm.execCommand("goLineUp"); eqPos(cm.getCursor(), {line: 0, ch: 0}); cm.execCommand("goLineDown"); - eqPos(cm.getCursor(), {line: 1, ch: 0}); + if (!phantom) // This fails in PhantomJS, though not in a real Webkit + eqPos(cm.getCursor(), {line: 1, ch: 0}); cm.setCursor({line: 1, ch: 12}); cm.execCommand("goLineDown"); eqPos(cm.getCursor(), {line: 2, ch: 5});