tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test.js (99016B)


      1 var Pos = CodeMirror.Pos;
      2 
      3 CodeMirror.defaults.rtlMoveVisually = true;
      4 
      5 function forEach(arr, f) {
      6  for (var i = 0, e = arr.length; i < e; ++i) f(arr[i], i);
      7 }
      8 
      9 function addDoc(cm, width, height) {
     10  var content = [], line = "";
     11  for (var i = 0; i < width; ++i) line += "x";
     12  for (var i = 0; i < height; ++i) content.push(line);
     13  cm.setValue(content.join("\n"));
     14 }
     15 
     16 function byClassName(elt, cls) {
     17  if (elt.getElementsByClassName) return elt.getElementsByClassName(cls);
     18  var found = [], re = new RegExp("\\b" + cls + "\\b");
     19  function search(elt) {
     20    if (elt.nodeType == 3) return;
     21    if (re.test(elt.className)) found.push(elt);
     22    for (var i = 0, e = elt.childNodes.length; i < e; ++i)
     23      search(elt.childNodes[i]);
     24  }
     25  search(elt);
     26  return found;
     27 }
     28 
     29 var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent);
     30 var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent);
     31 var mac = /Mac/.test(navigator.platform);
     32 var phantom = /PhantomJS/.test(navigator.userAgent);
     33 var opera = /Opera\/\./.test(navigator.userAgent);
     34 var opera_version = opera && navigator.userAgent.match(/Version\/(\d+\.\d+)/);
     35 if (opera_version) opera_version = Number(opera_version);
     36 var opera_lt10 = opera && (!opera_version || opera_version < 10);
     37 
     38 namespace = "core_";
     39 
     40 test("core_fromTextArea", function() {
     41  var te = document.getElementById("code");
     42  te.value = "CONTENT";
     43  var cm = CodeMirror.fromTextArea(te);
     44  is(!te.offsetHeight);
     45  eq(cm.getValue(), "CONTENT");
     46  cm.setValue("foo\nbar");
     47  eq(cm.getValue(), "foo\nbar");
     48  cm.save();
     49  is(/^foo\r?\nbar$/.test(te.value));
     50  cm.setValue("xxx");
     51  cm.toTextArea();
     52  is(te.offsetHeight);
     53  eq(te.value, "xxx");
     54 });
     55 
     56 testCM("getRange", function(cm) {
     57  eq(cm.getLine(0), "1234");
     58  eq(cm.getLine(1), "5678");
     59  eq(cm.getLine(2), null);
     60  eq(cm.getLine(-1), null);
     61  eq(cm.getRange(Pos(0, 0), Pos(0, 3)), "123");
     62  eq(cm.getRange(Pos(0, -1), Pos(0, 200)), "1234");
     63  eq(cm.getRange(Pos(0, 2), Pos(1, 2)), "34\n56");
     64  eq(cm.getRange(Pos(1, 2), Pos(100, 0)), "78");
     65 }, {value: "1234\n5678"});
     66 
     67 testCM("replaceRange", function(cm) {
     68  eq(cm.getValue(), "");
     69  cm.replaceRange("foo\n", Pos(0, 0));
     70  eq(cm.getValue(), "foo\n");
     71  cm.replaceRange("a\nb", Pos(0, 1));
     72  eq(cm.getValue(), "fa\nboo\n");
     73  eq(cm.lineCount(), 3);
     74  cm.replaceRange("xyzzy", Pos(0, 0), Pos(1, 1));
     75  eq(cm.getValue(), "xyzzyoo\n");
     76  cm.replaceRange("abc", Pos(0, 0), Pos(10, 0));
     77  eq(cm.getValue(), "abc");
     78  eq(cm.lineCount(), 1);
     79 });
     80 
     81 testCM("selection", function(cm) {
     82  cm.setSelection(Pos(0, 4), Pos(2, 2));
     83  is(cm.somethingSelected());
     84  eq(cm.getSelection(), "11\n222222\n33");
     85  eqCursorPos(cm.getCursor(false), Pos(2, 2));
     86  eqCursorPos(cm.getCursor(true), Pos(0, 4));
     87  cm.setSelection(Pos(1, 0));
     88  is(!cm.somethingSelected());
     89  eq(cm.getSelection(), "");
     90  eqCursorPos(cm.getCursor(true), Pos(1, 0));
     91  cm.replaceSelection("abc", "around");
     92  eq(cm.getSelection(), "abc");
     93  eq(cm.getValue(), "111111\nabc222222\n333333");
     94  cm.replaceSelection("def", "end");
     95  eq(cm.getSelection(), "");
     96  eqCursorPos(cm.getCursor(true), Pos(1, 3));
     97  cm.setCursor(Pos(2, 1));
     98  eqCursorPos(cm.getCursor(true), Pos(2, 1));
     99  cm.setCursor(1, 2);
    100  eqCursorPos(cm.getCursor(true), Pos(1, 2));
    101 }, {value: "111111\n222222\n333333"});
    102 
    103 testCM("extendSelection", function(cm) {
    104  cm.setExtending(true);
    105  addDoc(cm, 10, 10);
    106  cm.setSelection(Pos(3, 5));
    107  eqCursorPos(cm.getCursor("head"), Pos(3, 5));
    108  eqCursorPos(cm.getCursor("anchor"), Pos(3, 5));
    109  cm.setSelection(Pos(2, 5), Pos(5, 5));
    110  eqCursorPos(cm.getCursor("head"), Pos(5, 5));
    111  eqCursorPos(cm.getCursor("anchor"), Pos(2, 5));
    112  eqCursorPos(cm.getCursor("start"), Pos(2, 5));
    113  eqCursorPos(cm.getCursor("end"), Pos(5, 5));
    114  cm.setSelection(Pos(5, 5), Pos(2, 5));
    115  eqCursorPos(cm.getCursor("head"), Pos(2, 5));
    116  eqCursorPos(cm.getCursor("anchor"), Pos(5, 5));
    117  eqCursorPos(cm.getCursor("start"), Pos(2, 5));
    118  eqCursorPos(cm.getCursor("end"), Pos(5, 5));
    119  cm.extendSelection(Pos(3, 2));
    120  eqCursorPos(cm.getCursor("head"), Pos(3, 2));
    121  eqCursorPos(cm.getCursor("anchor"), Pos(5, 5));
    122  cm.extendSelection(Pos(6, 2));
    123  eqCursorPos(cm.getCursor("head"), Pos(6, 2));
    124  eqCursorPos(cm.getCursor("anchor"), Pos(5, 5));
    125  cm.extendSelection(Pos(6, 3), Pos(6, 4));
    126  eqCursorPos(cm.getCursor("head"), Pos(6, 4));
    127  eqCursorPos(cm.getCursor("anchor"), Pos(5, 5));
    128  cm.extendSelection(Pos(0, 3), Pos(0, 4));
    129  eqCursorPos(cm.getCursor("head"), Pos(0, 3));
    130  eqCursorPos(cm.getCursor("anchor"), Pos(5, 5));
    131  cm.extendSelection(Pos(4, 5), Pos(6, 5));
    132  eqCursorPos(cm.getCursor("head"), Pos(6, 5));
    133  eqCursorPos(cm.getCursor("anchor"), Pos(4, 5));
    134  cm.setExtending(false);
    135  cm.extendSelection(Pos(0, 3), Pos(0, 4));
    136  eqCursorPos(cm.getCursor("head"), Pos(0, 3));
    137  eqCursorPos(cm.getCursor("anchor"), Pos(0, 4));
    138 });
    139 
    140 testCM("lines", function(cm) {
    141  eq(cm.getLine(0), "111111");
    142  eq(cm.getLine(1), "222222");
    143  eq(cm.getLine(-1), null);
    144  cm.replaceRange("", Pos(1, 0), Pos(2, 0))
    145  cm.replaceRange("abc", Pos(1, 0), Pos(1));
    146  eq(cm.getValue(), "111111\nabc");
    147 }, {value: "111111\n222222\n333333"});
    148 
    149 testCM("indent", function(cm) {
    150  cm.indentLine(1);
    151  eq(cm.getLine(1), "   blah();");
    152  cm.setOption("indentUnit", 8);
    153  cm.indentLine(1);
    154  eq(cm.getLine(1), "\tblah();");
    155  cm.setOption("indentUnit", 10);
    156  cm.setOption("tabSize", 4);
    157  cm.indentLine(1);
    158  eq(cm.getLine(1), "\t\t  blah();");
    159 }, {value: "if (x) {\nblah();\n}", indentUnit: 3, indentWithTabs: true, tabSize: 8});
    160 
    161 testCM("indentByNumber", function(cm) {
    162  cm.indentLine(0, 2);
    163  eq(cm.getLine(0), "  foo");
    164  cm.indentLine(0, -200);
    165  eq(cm.getLine(0), "foo");
    166  cm.setSelection(Pos(0, 0), Pos(1, 2));
    167  cm.indentSelection(3);
    168  eq(cm.getValue(), "   foo\n   bar\nbaz");
    169 }, {value: "foo\nbar\nbaz"});
    170 
    171 test("core_defaults", function() {
    172  var defsCopy = {}, defs = CodeMirror.defaults;
    173  for (var opt in defs) defsCopy[opt] = defs[opt];
    174  defs.indentUnit = 5;
    175  defs.value = "uu";
    176  defs.indentWithTabs = true;
    177  defs.tabindex = 55;
    178  var place = document.getElementById("testground"), cm = CodeMirror(place);
    179  try {
    180    eq(cm.getOption("indentUnit"), 5);
    181    cm.setOption("indentUnit", 10);
    182    eq(defs.indentUnit, 5);
    183    eq(cm.getValue(), "uu");
    184    eq(cm.getOption("indentWithTabs"), true);
    185    eq(cm.getInputField().tabIndex, 55);
    186  }
    187  finally {
    188    for (var opt in defsCopy) defs[opt] = defsCopy[opt];
    189    place.removeChild(cm.getWrapperElement());
    190  }
    191 });
    192 
    193 testCM("lineInfo", function(cm) {
    194  eq(cm.lineInfo(-1), null);
    195  var mark = document.createElement("span");
    196  var lh = cm.setGutterMarker(1, "FOO", mark);
    197  var info = cm.lineInfo(1);
    198  eq(info.text, "222222");
    199  eq(info.gutterMarkers.FOO, mark);
    200  eq(info.line, 1);
    201  eq(cm.lineInfo(2).gutterMarkers, null);
    202  cm.setGutterMarker(lh, "FOO", null);
    203  eq(cm.lineInfo(1).gutterMarkers, null);
    204  cm.setGutterMarker(1, "FOO", mark);
    205  cm.setGutterMarker(0, "FOO", mark);
    206  cm.clearGutter("FOO");
    207  eq(cm.lineInfo(0).gutterMarkers, null);
    208  eq(cm.lineInfo(1).gutterMarkers, null);
    209 }, {value: "111111\n222222\n333333"});
    210 
    211 testCM("coords", function(cm) {
    212  cm.setSize(null, 100);
    213  addDoc(cm, 32, 200);
    214  var top = cm.charCoords(Pos(0, 0));
    215  var bot = cm.charCoords(Pos(200, 30));
    216  is(top.left < bot.left);
    217  is(top.top < bot.top);
    218  is(top.top < top.bottom);
    219  cm.scrollTo(null, 100);
    220  var top2 = cm.charCoords(Pos(0, 0));
    221  is(top.top > top2.top);
    222  eq(top.left, top2.left);
    223 });
    224 
    225 testCM("coordsChar", function(cm) {
    226  addDoc(cm, 35, 70);
    227  for (var i = 0; i < 2; ++i) {
    228    var sys = i ? "local" : "page";
    229    for (var ch = 0; ch <= 35; ch += 5) {
    230      for (var line = 0; line < 70; line += 5) {
    231        cm.setCursor(line, ch);
    232        var coords = cm.charCoords(Pos(line, ch), sys);
    233        var pos = cm.coordsChar({left: coords.left + 1, top: coords.top + 1}, sys);
    234        eqCharPos(pos, Pos(line, ch));
    235      }
    236    }
    237  }
    238 }, {lineNumbers: true});
    239 
    240 testCM("coordsCharBidi", function(cm) {
    241  addDoc(cm, 35, 70);
    242  // Put an rtl character into each line to trigger the bidi code path in coordsChar
    243  cm.setValue(cm.getValue().replace(/\bx/g, 'و'))
    244  for (var i = 0; i < 2; ++i) {
    245    var sys = i ? "local" : "page";
    246    for (var ch = 2; ch <= 35; ch += 5) {
    247      for (var line = 0; line < 70; line += 5) {
    248        cm.setCursor(line, ch);
    249        var coords = cm.charCoords(Pos(line, ch), sys);
    250        var pos = cm.coordsChar({left: coords.left + 1, top: coords.top + 1}, sys);
    251        eqCharPos(pos, Pos(line, ch));
    252      }
    253    }
    254  }
    255 }, {lineNumbers: true});
    256 
    257 testCM("badBidiOptimization", function(cm) {
    258  var coords = cm.charCoords(Pos(0, 34))
    259  eqCharPos(cm.coordsChar({left: coords.right, top: coords.top + 2}), Pos(0, 34))
    260 }, {value: "----------<p class=\"title\">هل يمكنك اختيار مستوى قسط التأمين الذي ترغب بدفعه؟</p>"})
    261 
    262 testCM("posFromIndex", function(cm) {
    263  cm.setValue(
    264    "This function should\n" +
    265    "convert a zero based index\n" +
    266    "to line and ch."
    267  );
    268 
    269  var examples = [
    270    { index: -1, line: 0, ch: 0  }, // <- Tests clipping
    271    { index: 0,  line: 0, ch: 0  },
    272    { index: 10, line: 0, ch: 10 },
    273    { index: 39, line: 1, ch: 18 },
    274    { index: 55, line: 2, ch: 7  },
    275    { index: 63, line: 2, ch: 15 },
    276    { index: 64, line: 2, ch: 15 }  // <- Tests clipping
    277  ];
    278 
    279  for (var i = 0; i < examples.length; i++) {
    280    var example = examples[i];
    281    var pos = cm.posFromIndex(example.index);
    282    eq(pos.line, example.line);
    283    eq(pos.ch, example.ch);
    284    if (example.index >= 0 && example.index < 64)
    285      eq(cm.indexFromPos(pos), example.index);
    286  }
    287 });
    288 
    289 testCM("undo", function(cm) {
    290  cm.replaceRange("def", Pos(0, 0), Pos(0));
    291  eq(cm.historySize().undo, 1);
    292  cm.undo();
    293  eq(cm.getValue(), "abc");
    294  eq(cm.historySize().undo, 0);
    295  eq(cm.historySize().redo, 1);
    296  cm.redo();
    297  eq(cm.getValue(), "def");
    298  eq(cm.historySize().undo, 1);
    299  eq(cm.historySize().redo, 0);
    300  cm.setValue("1\n\n\n2");
    301  cm.clearHistory();
    302  eq(cm.historySize().undo, 0);
    303  for (var i = 0; i < 20; ++i) {
    304    cm.replaceRange("a", Pos(0, 0));
    305    cm.replaceRange("b", Pos(3, 0));
    306  }
    307  eq(cm.historySize().undo, 40);
    308  for (var i = 0; i < 40; ++i)
    309    cm.undo();
    310  eq(cm.historySize().redo, 40);
    311  eq(cm.getValue(), "1\n\n\n2");
    312 }, {value: "abc"});
    313 
    314 testCM("undoDepth", function(cm) {
    315  cm.replaceRange("d", Pos(0));
    316  cm.replaceRange("e", Pos(0));
    317  cm.replaceRange("f", Pos(0));
    318  cm.undo(); cm.undo(); cm.undo();
    319  eq(cm.getValue(), "abcd");
    320 }, {value: "abc", undoDepth: 4});
    321 
    322 testCM("undoDoesntClearValue", function(cm) {
    323  cm.undo();
    324  eq(cm.getValue(), "x");
    325 }, {value: "x"});
    326 
    327 testCM("undoMultiLine", function(cm) {
    328  cm.operation(function() {
    329    cm.replaceRange("x", Pos(0, 0));
    330    cm.replaceRange("y", Pos(1, 0));
    331  });
    332  cm.undo();
    333  eq(cm.getValue(), "abc\ndef\nghi");
    334  cm.operation(function() {
    335    cm.replaceRange("y", Pos(1, 0));
    336    cm.replaceRange("x", Pos(0, 0));
    337  });
    338  cm.undo();
    339  eq(cm.getValue(), "abc\ndef\nghi");
    340  cm.operation(function() {
    341    cm.replaceRange("y", Pos(2, 0));
    342    cm.replaceRange("x", Pos(1, 0));
    343    cm.replaceRange("z", Pos(2, 0));
    344  });
    345  cm.undo();
    346  eq(cm.getValue(), "abc\ndef\nghi", 3);
    347 }, {value: "abc\ndef\nghi"});
    348 
    349 testCM("undoComposite", function(cm) {
    350  cm.replaceRange("y", Pos(1));
    351  cm.operation(function() {
    352    cm.replaceRange("x", Pos(0));
    353    cm.replaceRange("z", Pos(2));
    354  });
    355  eq(cm.getValue(), "ax\nby\ncz\n");
    356  cm.undo();
    357  eq(cm.getValue(), "a\nby\nc\n");
    358  cm.undo();
    359  eq(cm.getValue(), "a\nb\nc\n");
    360  cm.redo(); cm.redo();
    361  eq(cm.getValue(), "ax\nby\ncz\n");
    362 }, {value: "a\nb\nc\n"});
    363 
    364 testCM("undoSelection", function(cm) {
    365  cm.setSelection(Pos(0, 2), Pos(0, 4));
    366  cm.replaceSelection("");
    367  cm.setCursor(Pos(1, 0));
    368  cm.undo();
    369  eqCursorPos(cm.getCursor(true), Pos(0, 2));
    370  eqCursorPos(cm.getCursor(false), Pos(0, 4));
    371  cm.setCursor(Pos(1, 0));
    372  cm.redo();
    373  eqCursorPos(cm.getCursor(true), Pos(0, 2));
    374  eqCursorPos(cm.getCursor(false), Pos(0, 2));
    375 }, {value: "abcdefgh\n"});
    376 
    377 testCM("undoSelectionAsBefore", function(cm) {
    378  cm.replaceSelection("abc", "around");
    379  cm.undo();
    380  cm.redo();
    381  eq(cm.getSelection(), "abc");
    382 });
    383 
    384 testCM("selectionChangeConfusesHistory", function(cm) {
    385  cm.replaceSelection("abc", null, "dontmerge");
    386  cm.operation(function() {
    387    cm.setCursor(Pos(0, 0));
    388    cm.replaceSelection("abc", null, "dontmerge");
    389  });
    390  eq(cm.historySize().undo, 2);
    391 });
    392 
    393 testCM("markTextSingleLine", function(cm) {
    394  forEach([{a: 0, b: 1, c: "", f: 2, t: 5},
    395           {a: 0, b: 4, c: "", f: 0, t: 2},
    396           {a: 1, b: 2, c: "x", f: 3, t: 6},
    397           {a: 4, b: 5, c: "", f: 3, t: 5},
    398           {a: 4, b: 5, c: "xx", f: 3, t: 7},
    399           {a: 2, b: 5, c: "", f: 2, t: 3},
    400           {a: 2, b: 5, c: "abcd", f: 6, t: 7},
    401           {a: 2, b: 6, c: "x", f: null, t: null},
    402           {a: 3, b: 6, c: "", f: null, t: null},
    403           {a: 0, b: 9, c: "hallo", f: null, t: null},
    404           {a: 4, b: 6, c: "x", f: 3, t: 4},
    405           {a: 4, b: 8, c: "", f: 3, t: 4},
    406           {a: 6, b: 6, c: "a", f: 3, t: 6},
    407           {a: 8, b: 9, c: "", f: 3, t: 6}], function(test) {
    408    cm.setValue("1234567890");
    409    var r = cm.markText(Pos(0, 3), Pos(0, 6), {className: "foo"});
    410    cm.replaceRange(test.c, Pos(0, test.a), Pos(0, test.b));
    411    var f = r.find();
    412    eq(f && f.from.ch, test.f); eq(f && f.to.ch, test.t);
    413  });
    414 });
    415 
    416 testCM("markTextMultiLine", function(cm) {
    417  function p(v) { return v && Pos(v[0], v[1]); }
    418  forEach([{a: [0, 0], b: [0, 5], c: "", f: [0, 0], t: [2, 5]},
    419           {a: [0, 0], b: [0, 5], c: "foo\n", f: [1, 0], t: [3, 5]},
    420           {a: [0, 1], b: [0, 10], c: "", f: [0, 1], t: [2, 5]},
    421           {a: [0, 5], b: [0, 6], c: "x", f: [0, 6], t: [2, 5]},
    422           {a: [0, 0], b: [1, 0], c: "", f: [0, 0], t: [1, 5]},
    423           {a: [0, 6], b: [2, 4], c: "", f: [0, 5], t: [0, 7]},
    424           {a: [0, 6], b: [2, 4], c: "aa", f: [0, 5], t: [0, 9]},
    425           {a: [1, 2], b: [1, 8], c: "", f: [0, 5], t: [2, 5]},
    426           {a: [0, 5], b: [2, 5], c: "xx", f: null, t: null},
    427           {a: [0, 0], b: [2, 10], c: "x", f: null, t: null},
    428           {a: [1, 5], b: [2, 5], c: "", f: [0, 5], t: [1, 5]},
    429           {a: [2, 0], b: [2, 3], c: "", f: [0, 5], t: [2, 2]},
    430           {a: [2, 5], b: [3, 0], c: "a\nb", f: [0, 5], t: [2, 5]},
    431           {a: [2, 3], b: [3, 0], c: "x", f: [0, 5], t: [2, 3]},
    432           {a: [1, 1], b: [1, 9], c: "1\n2\n3", f: [0, 5], t: [4, 5]}], function(test) {
    433    cm.setValue("aaaaaaaaaa\nbbbbbbbbbb\ncccccccccc\ndddddddd\n");
    434    var r = cm.markText(Pos(0, 5), Pos(2, 5),
    435                        {className: "CodeMirror-matchingbracket"});
    436    cm.replaceRange(test.c, p(test.a), p(test.b));
    437    var f = r.find();
    438    eqCursorPos(f && f.from, p(test.f)); eqCursorPos(f && f.to, p(test.t));
    439  });
    440 });
    441 
    442 testCM("markTextUndo", function(cm) {
    443  var marker1, marker2, bookmark;
    444  marker1 = cm.markText(Pos(0, 1), Pos(0, 3),
    445                        {className: "CodeMirror-matchingbracket"});
    446  marker2 = cm.markText(Pos(0, 0), Pos(2, 1),
    447                        {className: "CodeMirror-matchingbracket"});
    448  bookmark = cm.setBookmark(Pos(1, 5));
    449  cm.operation(function(){
    450    cm.replaceRange("foo", Pos(0, 2));
    451    cm.replaceRange("bar\nbaz\nbug\n", Pos(2, 0), Pos(3, 0));
    452  });
    453  var v1 = cm.getValue();
    454  cm.setValue("");
    455  eq(marker1.find(), null); eq(marker2.find(), null); eq(bookmark.find(), null);
    456  cm.undo();
    457  eqCursorPos(bookmark.find(), Pos(1, 5), "still there");
    458  cm.undo();
    459  var m1Pos = marker1.find(), m2Pos = marker2.find();
    460  eqCursorPos(m1Pos.from, Pos(0, 1)); eqCursorPos(m1Pos.to, Pos(0, 3));
    461  eqCursorPos(m2Pos.from, Pos(0, 0)); eqCursorPos(m2Pos.to, Pos(2, 1));
    462  eqCursorPos(bookmark.find(), Pos(1, 5));
    463  cm.redo(); cm.redo();
    464  eq(bookmark.find(), null);
    465  cm.undo();
    466  eqCursorPos(bookmark.find(), Pos(1, 5));
    467  eq(cm.getValue(), v1);
    468 }, {value: "1234\n56789\n00\n"});
    469 
    470 testCM("markTextStayGone", function(cm) {
    471  var m1 = cm.markText(Pos(0, 0), Pos(0, 1));
    472  cm.replaceRange("hi", Pos(0, 2));
    473  m1.clear();
    474  cm.undo();
    475  eq(m1.find(), null);
    476 }, {value: "hello"});
    477 
    478 testCM("markTextAllowEmpty", function(cm) {
    479  var m1 = cm.markText(Pos(0, 1), Pos(0, 2), {clearWhenEmpty: false});
    480  is(m1.find());
    481  cm.replaceRange("x", Pos(0, 0));
    482  is(m1.find());
    483  cm.replaceRange("y", Pos(0, 2));
    484  is(m1.find());
    485  cm.replaceRange("z", Pos(0, 3), Pos(0, 4));
    486  is(!m1.find());
    487  var m2 = cm.markText(Pos(0, 1), Pos(0, 2), {clearWhenEmpty: false,
    488                                              inclusiveLeft: true,
    489                                              inclusiveRight: true});
    490  cm.replaceRange("q", Pos(0, 1), Pos(0, 2));
    491  is(m2.find());
    492  cm.replaceRange("", Pos(0, 0), Pos(0, 3));
    493  is(!m2.find());
    494  var m3 = cm.markText(Pos(0, 1), Pos(0, 1), {clearWhenEmpty: false});
    495  cm.replaceRange("a", Pos(0, 3));
    496  is(m3.find());
    497  cm.replaceRange("b", Pos(0, 1));
    498  is(!m3.find());
    499 }, {value: "abcde"});
    500 
    501 testCM("markTextStacked", function(cm) {
    502  var m1 = cm.markText(Pos(0, 0), Pos(0, 0), {clearWhenEmpty: false});
    503  var m2 = cm.markText(Pos(0, 0), Pos(0, 0), {clearWhenEmpty: false});
    504  cm.replaceRange("B", Pos(0, 1));
    505  is(m1.find() && m2.find());
    506 }, {value: "A"});
    507 
    508 testCM("undoPreservesNewMarks", function(cm) {
    509  cm.markText(Pos(0, 3), Pos(0, 4));
    510  cm.markText(Pos(1, 1), Pos(1, 3));
    511  cm.replaceRange("", Pos(0, 3), Pos(3, 1));
    512  var mBefore = cm.markText(Pos(0, 0), Pos(0, 1));
    513  var mAfter = cm.markText(Pos(0, 5), Pos(0, 6));
    514  var mAround = cm.markText(Pos(0, 2), Pos(0, 4));
    515  cm.undo();
    516  eqCursorPos(mBefore.find().from, Pos(0, 0));
    517  eqCursorPos(mBefore.find().to, Pos(0, 1));
    518  eqCursorPos(mAfter.find().from, Pos(3, 3));
    519  eqCursorPos(mAfter.find().to, Pos(3, 4));
    520  eqCursorPos(mAround.find().from, Pos(0, 2));
    521  eqCursorPos(mAround.find().to, Pos(3, 2));
    522  var found = cm.findMarksAt(Pos(2, 2));
    523  eq(found.length, 1);
    524  eq(found[0], mAround);
    525 }, {value: "aaaa\nbbbb\ncccc\ndddd"});
    526 
    527 testCM("markClearBetween", function(cm) {
    528  cm.setValue("aaa\nbbb\nccc\nddd\n");
    529  cm.markText(Pos(0, 0), Pos(2));
    530  cm.replaceRange("aaa\nbbb\nccc", Pos(0, 0), Pos(2));
    531  eq(cm.findMarksAt(Pos(1, 1)).length, 0);
    532 });
    533 
    534 testCM("findMarksMiddle", function(cm) {
    535  var mark = cm.markText(Pos(1, 1), Pos(3, 1));
    536  var found = cm.findMarks(Pos(2, 1), Pos(2, 2));
    537  eq(found.length, 1);
    538  eq(found[0], mark);
    539 }, {value: "line 0\nline 1\nline 2\nline 3"});
    540 
    541 testCM("deleteSpanCollapsedInclusiveLeft", function(cm) {
    542  var from = Pos(1, 0), to = Pos(1, 1);
    543  var m = cm.markText(from, to, {collapsed: true, inclusiveLeft: true});
    544  // Delete collapsed span.
    545  cm.replaceRange("", from, to);
    546 }, {value: "abc\nX\ndef"});
    547 
    548 testCM("markTextCSS", function(cm) {
    549  function present() {
    550    var spans = cm.display.lineDiv.getElementsByTagName("span");
    551    for (var i = 0; i < spans.length; i++)
    552      if (spans[i].style.color && spans[i].textContent == "cdef") return true;
    553  }
    554  var m = cm.markText(Pos(0, 2), Pos(0, 6), {css: "color: cyan"});
    555  is(present());
    556  m.clear();
    557  is(!present());
    558 }, {value: "abcdefgh"});
    559 
    560 testCM("markTextWithAttributes", function(cm) {
    561  function present() {
    562    var spans = cm.display.lineDiv.getElementsByTagName("span");
    563    for (var i = 0; i < spans.length; i++)
    564      if (spans[i].getAttribute("label") == "label" && spans[i].textContent == "cdef") return true;
    565  }
    566  var m = cm.markText(Pos(0, 2), Pos(0, 6), {attributes: {label: "label"}});
    567  is(present());
    568  m.clear();
    569  is(!present());
    570 }, {value: "abcdefgh"});
    571 
    572 testCM("bookmark", function(cm) {
    573  function p(v) { return v && Pos(v[0], v[1]); }
    574  forEach([{a: [1, 0], b: [1, 1], c: "", d: [1, 4]},
    575           {a: [1, 1], b: [1, 1], c: "xx", d: [1, 7]},
    576           {a: [1, 4], b: [1, 5], c: "ab", d: [1, 6]},
    577           {a: [1, 4], b: [1, 6], c: "", d: null},
    578           {a: [1, 5], b: [1, 6], c: "abc", d: [1, 5]},
    579           {a: [1, 6], b: [1, 8], c: "", d: [1, 5]},
    580           {a: [1, 4], b: [1, 4], c: "\n\n", d: [3, 1]},
    581           {bm: [1, 9], a: [1, 1], b: [1, 1], c: "\n", d: [2, 8]}], function(test) {
    582    cm.setValue("1234567890\n1234567890\n1234567890");
    583    var b = cm.setBookmark(p(test.bm) || Pos(1, 5));
    584    cm.replaceRange(test.c, p(test.a), p(test.b));
    585    eqCursorPos(b.find(), p(test.d));
    586  });
    587 });
    588 
    589 testCM("bookmarkInsertLeft", function(cm) {
    590  var br = cm.setBookmark(Pos(0, 2), {insertLeft: false});
    591  var bl = cm.setBookmark(Pos(0, 2), {insertLeft: true});
    592  cm.setCursor(Pos(0, 2));
    593  cm.replaceSelection("hi");
    594  eqCursorPos(br.find(), Pos(0, 2));
    595  eqCursorPos(bl.find(), Pos(0, 4));
    596  cm.replaceRange("", Pos(0, 4), Pos(0, 5));
    597  cm.replaceRange("", Pos(0, 2), Pos(0, 4));
    598  cm.replaceRange("", Pos(0, 1), Pos(0, 2));
    599  // Verify that deleting next to bookmarks doesn't kill them
    600  eqCursorPos(br.find(), Pos(0, 1));
    601  eqCursorPos(bl.find(), Pos(0, 1));
    602 }, {value: "abcdef"});
    603 
    604 testCM("bookmarkCursor", function(cm) {
    605  var pos01 = cm.cursorCoords(Pos(0, 1)), pos11 = cm.cursorCoords(Pos(1, 1)),
    606      pos20 = cm.cursorCoords(Pos(2, 0)), pos30 = cm.cursorCoords(Pos(3, 0)),
    607      pos41 = cm.cursorCoords(Pos(4, 1));
    608  cm.setBookmark(Pos(0, 1), {widget: document.createTextNode("←"), insertLeft: true});
    609  cm.setBookmark(Pos(2, 0), {widget: document.createTextNode("←"), insertLeft: true});
    610  cm.setBookmark(Pos(1, 1), {widget: document.createTextNode("→")});
    611  cm.setBookmark(Pos(3, 0), {widget: document.createTextNode("→")});
    612  var new01 = cm.cursorCoords(Pos(0, 1)), new11 = cm.cursorCoords(Pos(1, 1)),
    613      new20 = cm.cursorCoords(Pos(2, 0)), new30 = cm.cursorCoords(Pos(3, 0));
    614  near(new01.left, pos01.left, 1);
    615  near(new01.top, pos01.top, 1);
    616  is(new11.left > pos11.left, "at right, middle of line");
    617  near(new11.top == pos11.top, 1);
    618  near(new20.left, pos20.left, 1);
    619  near(new20.top, pos20.top, 1);
    620  is(new30.left > pos30.left, "at right, empty line");
    621  near(new30.top, pos30, 1);
    622  cm.setBookmark(Pos(4, 0), {widget: document.createTextNode("→")});
    623  is(cm.cursorCoords(Pos(4, 1)).left > pos41.left, "single-char bug");
    624 }, {value: "foo\nbar\n\n\nx\ny"});
    625 
    626 testCM("multiBookmarkCursor", function(cm) {
    627  if (phantom) return;
    628  var ms = [], m;
    629  function add(insertLeft) {
    630    for (var i = 0; i < 3; ++i) {
    631      var node = document.createElement("span");
    632      node.innerHTML = "X";
    633      ms.push(cm.setBookmark(Pos(0, 1), {widget: node, insertLeft: insertLeft}));
    634    }
    635  }
    636  var base1 = cm.cursorCoords(Pos(0, 1)).left, base4 = cm.cursorCoords(Pos(0, 4)).left;
    637  add(true);
    638  near(base1, cm.cursorCoords(Pos(0, 1)).left, 1);
    639  while (m = ms.pop()) m.clear();
    640  add(false);
    641  near(base4, cm.cursorCoords(Pos(0, 1)).left, 1);
    642 }, {value: "abcdefg"});
    643 
    644 testCM("getAllMarks", function(cm) {
    645  addDoc(cm, 10, 10);
    646  var m1 = cm.setBookmark(Pos(0, 2));
    647  var m2 = cm.markText(Pos(0, 2), Pos(3, 2));
    648  var m3 = cm.markText(Pos(1, 2), Pos(1, 8));
    649  var m4 = cm.markText(Pos(8, 0), Pos(9, 0));
    650  eq(cm.getAllMarks().length, 4);
    651  m1.clear();
    652  m3.clear();
    653  eq(cm.getAllMarks().length, 2);
    654 });
    655 
    656 testCM("setValueClears", function(cm) {
    657  cm.addLineClass(0, "wrap", "foo");
    658  var mark = cm.markText(Pos(0, 0), Pos(1, 1), {inclusiveLeft: true, inclusiveRight: true});
    659  cm.setValue("foo");
    660  is(!cm.lineInfo(0).wrapClass);
    661  is(!mark.find());
    662 }, {value: "a\nb"});
    663 
    664 testCM("bug577", function(cm) {
    665  cm.setValue("a\nb");
    666  cm.clearHistory();
    667  cm.setValue("fooooo");
    668  cm.undo();
    669 });
    670 
    671 testCM("scrollSnap", function(cm) {
    672  cm.setSize(100, 100);
    673  addDoc(cm, 200, 200);
    674  cm.setCursor(Pos(100, 180));
    675  var info = cm.getScrollInfo();
    676  is(info.left > 0 && info.top > 0);
    677  cm.setCursor(Pos(0, 0));
    678  info = cm.getScrollInfo();
    679  is(info.left == 0 && info.top == 0, "scrolled clean to top");
    680  cm.setCursor(Pos(100, 180));
    681  cm.setCursor(Pos(199, 0));
    682  info = cm.getScrollInfo();
    683  is(info.left == 0 && info.top + 2 > info.height - cm.getScrollerElement().clientHeight, "scrolled clean to bottom");
    684 });
    685 
    686 testCM("scrollIntoView", function(cm) {
    687  if (phantom) return;
    688  function test(line, ch, msg) {
    689    var pos = Pos(line, ch);
    690    cm.scrollIntoView(pos);
    691    var outer = cm.getWrapperElement().getBoundingClientRect();
    692    var box = cm.charCoords(pos, "window");
    693    is(box.left >= outer.left, msg + " (left)");
    694    is(box.right <= outer.right, msg + " (right)");
    695    is(box.top >= outer.top, msg + " (top)");
    696    is(box.bottom <= outer.bottom, msg + " (bottom)");
    697  }
    698  addDoc(cm, 200, 200);
    699  test(199, 199, "bottom right");
    700  test(0, 0, "top left");
    701  test(100, 100, "center");
    702  test(199, 0, "bottom left");
    703  test(0, 199, "top right");
    704  test(100, 100, "center again");
    705 });
    706 
    707 testCM("scrollBackAndForth", function(cm) {
    708  addDoc(cm, 1, 200);
    709  cm.operation(function() {
    710    cm.scrollIntoView(Pos(199, 0));
    711    cm.scrollIntoView(Pos(4, 0));
    712  });
    713  is(cm.getScrollInfo().top > 0);
    714 });
    715 
    716 testCM("selectAllNoScroll", function(cm) {
    717  addDoc(cm, 1, 200);
    718  cm.execCommand("selectAll");
    719  eq(cm.getScrollInfo().top, 0);
    720  cm.setCursor(199);
    721  cm.execCommand("selectAll");
    722  is(cm.getScrollInfo().top > 0);
    723 });
    724 
    725 testCM("selectionPos", function(cm) {
    726  if (phantom || cm.getOption("inputStyle") != "textarea") return;
    727  cm.setSize(100, 100);
    728  addDoc(cm, 200, 100);
    729  cm.setSelection(Pos(1, 100), Pos(98, 100));
    730  var lineWidth = cm.charCoords(Pos(0, 200), "local").left;
    731  var lineHeight = (cm.charCoords(Pos(99)).top - cm.charCoords(Pos(0)).top) / 100;
    732  cm.scrollTo(0, 0);
    733  var selElt = byClassName(cm.getWrapperElement(), "CodeMirror-selected");
    734  var outer = cm.getWrapperElement().getBoundingClientRect();
    735  var sawMiddle, sawTop, sawBottom;
    736  for (var i = 0, e = selElt.length; i < e; ++i) {
    737    var box = selElt[i].getBoundingClientRect();
    738    var atLeft = box.left - outer.left < 30;
    739    var width = box.right - box.left;
    740    var atRight = box.right - outer.left > .8 * lineWidth;
    741    if (atLeft && atRight) {
    742      sawMiddle = true;
    743      is(box.bottom - box.top > 90 * lineHeight, "middle high");
    744      is(width > .9 * lineWidth, "middle wide");
    745    } else {
    746      is(width > .4 * lineWidth, "top/bot wide enough");
    747      is(width < .6 * lineWidth, "top/bot slim enough");
    748      if (atLeft) {
    749        sawBottom = true;
    750        is(box.top - outer.top > 96 * lineHeight, "bot below");
    751      } else if (atRight) {
    752        sawTop = true;
    753        is(box.top - outer.top < 2.1 * lineHeight, "top above");
    754      }
    755    }
    756  }
    757  is(sawTop && sawBottom && sawMiddle, "all parts");
    758 }, null);
    759 
    760 testCM("restoreHistory", function(cm) {
    761  cm.setValue("abc\ndef");
    762  cm.replaceRange("hello", Pos(1, 0), Pos(1));
    763  cm.replaceRange("goop", Pos(0, 0), Pos(0));
    764  cm.undo();
    765  var storedVal = cm.getValue(), storedHist = cm.getHistory();
    766  if (window.JSON) storedHist = JSON.parse(JSON.stringify(storedHist));
    767  eq(storedVal, "abc\nhello");
    768  cm.setValue("");
    769  cm.clearHistory();
    770  eq(cm.historySize().undo, 0);
    771  cm.setValue(storedVal);
    772  cm.setHistory(storedHist);
    773  cm.redo();
    774  eq(cm.getValue(), "goop\nhello");
    775  cm.undo(); cm.undo();
    776  eq(cm.getValue(), "abc\ndef");
    777 });
    778 
    779 testCM("doubleScrollbar", function(cm) {
    780  var dummy = document.body.appendChild(document.createElement("p"));
    781  dummy.style.cssText = "height: 50px; overflow: scroll; width: 50px";
    782  var scrollbarWidth = dummy.offsetWidth + 1 - dummy.clientWidth;
    783  document.body.removeChild(dummy);
    784  if (scrollbarWidth < 2) return;
    785  cm.setSize(null, 100);
    786  addDoc(cm, 1, 300);
    787  var wrap = cm.getWrapperElement();
    788  is(wrap.offsetWidth - byClassName(wrap, "CodeMirror-lines")[0].offsetWidth <= scrollbarWidth * 1.5);
    789 });
    790 
    791 testCM("weirdLinebreaks", function(cm) {
    792  cm.setValue("foo\nbar\rbaz\r\nquux\n\rplop");
    793  is(cm.getValue(), "foo\nbar\nbaz\nquux\n\nplop");
    794  is(cm.lineCount(), 6);
    795  cm.setValue("\n\n");
    796  is(cm.lineCount(), 3);
    797 });
    798 
    799 testCM("setSize", function(cm) {
    800  cm.setSize(100, 100);
    801  var wrap = cm.getWrapperElement();
    802  is(wrap.offsetWidth, 100);
    803  is(wrap.offsetHeight, 100);
    804  cm.setSize("100%", "3em");
    805  is(wrap.style.width, "100%");
    806  is(wrap.style.height, "3em");
    807  cm.setSize(null, 40);
    808  is(wrap.style.width, "100%");
    809  is(wrap.style.height, "40px");
    810 });
    811 
    812 function foldLines(cm, start, end, autoClear) {
    813  return cm.markText(Pos(start, 0), Pos(end - 1), {
    814    inclusiveLeft: true,
    815    inclusiveRight: true,
    816    collapsed: true,
    817    clearOnEnter: autoClear
    818  });
    819 }
    820 
    821 testCM("collapsedLines", function(cm) {
    822  addDoc(cm, 4, 10);
    823  var range = foldLines(cm, 4, 5), cleared = 0;
    824  CodeMirror.on(range, "clear", function() {cleared++;});
    825  cm.setCursor(Pos(3, 0));
    826  CodeMirror.commands.goLineDown(cm);
    827  eqCharPos(cm.getCursor(), Pos(5, 0));
    828  cm.replaceRange("abcdefg", Pos(3, 0), Pos(3));
    829  cm.setCursor(Pos(3, 6));
    830  CodeMirror.commands.goLineDown(cm);
    831  eqCharPos(cm.getCursor(), Pos(5, 4));
    832  cm.replaceRange("ab", Pos(3, 0), Pos(3));
    833  cm.setCursor(Pos(3, 2));
    834  CodeMirror.commands.goLineDown(cm);
    835  eqCharPos(cm.getCursor(), Pos(5, 2));
    836  cm.operation(function() {range.clear(); range.clear();});
    837  eq(cleared, 1);
    838 });
    839 
    840 testCM("collapsedRangeCoordsChar", function(cm) {
    841  var pos_1_3 = cm.charCoords(Pos(1, 3));
    842  pos_1_3.left += 2; pos_1_3.top += 2;
    843  var opts = {collapsed: true, inclusiveLeft: true, inclusiveRight: true};
    844  var m1 = cm.markText(Pos(0, 0), Pos(2, 0), opts);
    845  eqCharPos(cm.coordsChar(pos_1_3), Pos(3, 3));
    846  m1.clear();
    847  var m1 = cm.markText(Pos(0, 0), Pos(1, 1), {collapsed: true, inclusiveLeft: true});
    848  var m2 = cm.markText(Pos(1, 1), Pos(2, 0), {collapsed: true, inclusiveRight: true});
    849  eqCharPos(cm.coordsChar(pos_1_3), Pos(3, 3));
    850  m1.clear(); m2.clear();
    851  var m1 = cm.markText(Pos(0, 0), Pos(1, 6), opts);
    852  eqCharPos(cm.coordsChar(pos_1_3), Pos(3, 3));
    853 }, {value: "123456\nabcdef\nghijkl\nmnopqr\n"});
    854 
    855 testCM("collapsedRangeBetweenLinesSelected", function(cm) {
    856  if (cm.getOption("inputStyle") != "textarea") return;
    857  var widget = document.createElement("span");
    858  widget.textContent = "\u2194";
    859  cm.markText(Pos(0, 3), Pos(1, 0), {replacedWith: widget});
    860  cm.setSelection(Pos(0, 3), Pos(1, 0));
    861  var selElts = byClassName(cm.getWrapperElement(), "CodeMirror-selected");
    862  for (var i = 0, w = 0; i < selElts.length; i++)
    863    w += selElts[i].offsetWidth;
    864  is(w > 0);
    865 }, {value: "one\ntwo"});
    866 
    867 testCM("randomCollapsedRanges", function(cm) {
    868  addDoc(cm, 20, 500);
    869  cm.operation(function() {
    870    for (var i = 0; i < 200; i++) {
    871      var start = Pos(Math.floor(Math.random() * 500), Math.floor(Math.random() * 20));
    872      if (i % 4)
    873        try { cm.markText(start, Pos(start.line + 2, 1), {collapsed: true}); }
    874        catch(e) { if (!/overlapping/.test(String(e))) throw e; }
    875      else
    876        cm.markText(start, Pos(start.line, start.ch + 4), {"className": "foo"});
    877    }
    878  });
    879 });
    880 
    881 testCM("hiddenLinesAutoUnfold", function(cm) {
    882  var range = foldLines(cm, 1, 3, true), cleared = 0;
    883  CodeMirror.on(range, "clear", function() {cleared++;});
    884  cm.setCursor(Pos(3, 0));
    885  eq(cleared, 0);
    886  cm.execCommand("goCharLeft");
    887  eq(cleared, 1);
    888  range = foldLines(cm, 1, 3, true);
    889  CodeMirror.on(range, "clear", function() {cleared++;});
    890  eqCursorPos(cm.getCursor(), Pos(3, 0));
    891  cm.setCursor(Pos(0, 3));
    892  cm.execCommand("goCharRight");
    893  eq(cleared, 2);
    894 }, {value: "abc\ndef\nghi\njkl"});
    895 
    896 testCM("hiddenLinesSelectAll", function(cm) {  // Issue #484
    897  addDoc(cm, 4, 20);
    898  foldLines(cm, 0, 10);
    899  foldLines(cm, 11, 20);
    900  CodeMirror.commands.selectAll(cm);
    901  eqCursorPos(cm.getCursor(true), Pos(10, 0));
    902  eqCursorPos(cm.getCursor(false), Pos(10, 4));
    903 });
    904 
    905 testCM("clickFold", function(cm) { // Issue #5392
    906  cm.setValue("foo { bar }")
    907  var widget = document.createElement("span")
    908  widget.textContent = "<>"
    909  cm.markText(Pos(0, 5), Pos(0, 10), {replacedWith: widget})
    910  var after = cm.charCoords(Pos(0, 10))
    911  var foundOn = cm.coordsChar({left: after.left - 1, top: after.top + 4})
    912  is(foundOn.ch <= 5 || foundOn.ch >= 10, "Position is not inside the folded range")
    913 })
    914 
    915 testCM("everythingFolded", function(cm) {
    916  addDoc(cm, 2, 2);
    917  function enterPress() {
    918    cm.triggerOnKeyDown({type: "keydown", keyCode: 13, preventDefault: function(){}, stopPropagation: function(){}});
    919  }
    920  var fold = foldLines(cm, 0, 2);
    921  enterPress();
    922  eq(cm.getValue(), "xx\nxx");
    923  fold.clear();
    924  fold = foldLines(cm, 0, 2, true);
    925  eq(fold.find(), null);
    926  enterPress();
    927  eq(cm.getValue(), "\nxx\nxx");
    928 });
    929 
    930 testCM("structuredFold", function(cm) {
    931  if (phantom) return;
    932  addDoc(cm, 4, 8);
    933  var range = cm.markText(Pos(1, 2), Pos(6, 2), {
    934    replacedWith: document.createTextNode("Q")
    935  });
    936  cm.setCursor(0, 3);
    937  CodeMirror.commands.goLineDown(cm);
    938  eqCharPos(cm.getCursor(), Pos(6, 2));
    939  CodeMirror.commands.goCharLeft(cm);
    940  eqCharPos(cm.getCursor(), Pos(1, 2));
    941  CodeMirror.commands.delCharAfter(cm);
    942  eq(cm.getValue(), "xxxx\nxxxx\nxxxx");
    943  addDoc(cm, 4, 8);
    944  range = cm.markText(Pos(1, 2), Pos(6, 2), {
    945    replacedWith: document.createTextNode("M"),
    946    clearOnEnter: true
    947  });
    948  var cleared = 0;
    949  CodeMirror.on(range, "clear", function(){++cleared;});
    950  cm.setCursor(0, 3);
    951  CodeMirror.commands.goLineDown(cm);
    952  eqCharPos(cm.getCursor(), Pos(6, 2));
    953  CodeMirror.commands.goCharLeft(cm);
    954  eqCharPos(cm.getCursor(), Pos(6, 1));
    955  eq(cleared, 1);
    956  range.clear();
    957  eq(cleared, 1);
    958  range = cm.markText(Pos(1, 2), Pos(6, 2), {
    959    replacedWith: document.createTextNode("Q"),
    960    clearOnEnter: true
    961  });
    962  range.clear();
    963  cm.setCursor(1, 2);
    964  CodeMirror.commands.goCharRight(cm);
    965  eqCharPos(cm.getCursor(), Pos(1, 3));
    966  range = cm.markText(Pos(2, 0), Pos(4, 4), {
    967    replacedWith: document.createTextNode("M")
    968  });
    969  cm.setCursor(1, 0);
    970  CodeMirror.commands.goLineDown(cm);
    971  eqCharPos(cm.getCursor(), Pos(2, 0));
    972 }, null);
    973 
    974 testCM("nestedFold", function(cm) {
    975  addDoc(cm, 10, 3);
    976  function fold(ll, cl, lr, cr) {
    977    return cm.markText(Pos(ll, cl), Pos(lr, cr), {collapsed: true});
    978  }
    979  var inner1 = fold(0, 6, 1, 3), inner2 = fold(0, 2, 1, 8), outer = fold(0, 1, 2, 3), inner0 = fold(0, 5, 0, 6);
    980  cm.setCursor(0, 1);
    981  CodeMirror.commands.goCharRight(cm);
    982  eqCursorPos(cm.getCursor(), Pos(2, 3));
    983  inner0.clear();
    984  CodeMirror.commands.goCharLeft(cm);
    985  eqCursorPos(cm.getCursor(), Pos(0, 1));
    986  outer.clear();
    987  CodeMirror.commands.goCharRight(cm);
    988  eqCursorPos(cm.getCursor(), Pos(0, 2, "before"));
    989  CodeMirror.commands.goCharRight(cm);
    990  eqCursorPos(cm.getCursor(), Pos(1, 8));
    991  inner2.clear();
    992  CodeMirror.commands.goCharLeft(cm);
    993  eqCursorPos(cm.getCursor(), Pos(1, 7, "after"));
    994  cm.setCursor(0, 5);
    995  CodeMirror.commands.goCharRight(cm);
    996  eqCursorPos(cm.getCursor(), Pos(0, 6, "before"));
    997  CodeMirror.commands.goCharRight(cm);
    998  eqCursorPos(cm.getCursor(), Pos(1, 3));
    999 });
   1000 
   1001 testCM("badNestedFold", function(cm) {
   1002  addDoc(cm, 4, 4);
   1003  cm.markText(Pos(0, 2), Pos(3, 2), {collapsed: true});
   1004  var caught;
   1005  try {cm.markText(Pos(0, 1), Pos(0, 3), {collapsed: true});}
   1006  catch(e) {caught = e;}
   1007  is(caught instanceof Error, "no error");
   1008  is(/overlap/i.test(caught.message), "wrong error");
   1009 });
   1010 
   1011 testCM("nestedFoldOnSide", function(cm) {
   1012  var m1 = cm.markText(Pos(0, 1), Pos(2, 1), {collapsed: true, inclusiveRight: true});
   1013  var m2 = cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true});
   1014  cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true}).clear();
   1015  try { cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true, inclusiveLeft: true}); }
   1016  catch(e) { var caught = e; }
   1017  is(caught && /overlap/i.test(caught.message));
   1018  var m3 = cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true});
   1019  var m4 = cm.markText(Pos(2, 0), Pos(2, 1), {collapse: true, inclusiveRight: true});
   1020  m1.clear(); m4.clear();
   1021  m1 = cm.markText(Pos(0, 1), Pos(2, 1), {collapsed: true});
   1022  cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true}).clear();
   1023  try { cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true, inclusiveRight: true}); }
   1024  catch(e) { var caught = e; }
   1025  is(caught && /overlap/i.test(caught.message));
   1026 }, {value: "ab\ncd\ef"});
   1027 
   1028 testCM("editInFold", function(cm) {
   1029  addDoc(cm, 4, 6);
   1030  var m = cm.markText(Pos(1, 2), Pos(3, 2), {collapsed: true});
   1031  cm.replaceRange("", Pos(0, 0), Pos(1, 3));
   1032  cm.replaceRange("", Pos(2, 1), Pos(3, 3));
   1033  cm.replaceRange("a\nb\nc\nd", Pos(0, 1), Pos(1, 0));
   1034  cm.cursorCoords(Pos(0, 0));
   1035 });
   1036 
   1037 testCM("wrappingInlineWidget", function(cm) {
   1038  cm.setSize("11em");
   1039  var w = document.createElement("span");
   1040  w.style.color = "red";
   1041  w.innerHTML = "one two three four";
   1042  cm.markText(Pos(0, 6), Pos(0, 9), {replacedWith: w});
   1043  var cur0 = cm.cursorCoords(Pos(0, 0)), cur1 = cm.cursorCoords(Pos(0, 10));
   1044  is(cur0.top < cur1.top);
   1045  is(cur0.bottom < cur1.bottom);
   1046  var curL = cm.cursorCoords(Pos(0, 6)), curR = cm.cursorCoords(Pos(0, 9));
   1047  eq(curL.top, cur0.top);
   1048  eq(curL.bottom, cur0.bottom);
   1049  eq(curR.top, cur1.top);
   1050  eq(curR.bottom, cur1.bottom);
   1051  cm.replaceRange("", Pos(0, 9), Pos(0));
   1052  curR = cm.cursorCoords(Pos(0, 9));
   1053  if (phantom) return;
   1054  eq(curR.top, cur1.top);
   1055  eq(curR.bottom, cur1.bottom);
   1056 }, {value: "1 2 3 xxx 4", lineWrapping: true});
   1057 
   1058 testCM("showEmptyWidgetSpan", function(cm) {
   1059  var marker = cm.markText(Pos(0, 2), Pos(0, 2), {
   1060    clearWhenEmpty: false,
   1061    replacedWith: document.createTextNode("X")
   1062  });
   1063  var text = cm.display.view[0].text;
   1064  eq(text.textContent || text.innerText, "abXc");
   1065 }, {value: "abc"});
   1066 
   1067 testCM("changedInlineWidget", function(cm) {
   1068  cm.setSize("10em");
   1069  var w = document.createElement("span");
   1070  w.innerHTML = "x";
   1071  var m = cm.markText(Pos(0, 4), Pos(0, 5), {replacedWith: w});
   1072  w.innerHTML = "and now the widget is really really long all of a sudden and a scrollbar is needed";
   1073  m.changed();
   1074  var hScroll = byClassName(cm.getWrapperElement(), "CodeMirror-hscrollbar")[0];
   1075  is(hScroll.scrollWidth > hScroll.clientWidth);
   1076 }, {value: "hello there"});
   1077 
   1078 testCM("changedBookmark", function(cm) {
   1079  cm.setSize("10em");
   1080  var w = document.createElement("span");
   1081  w.innerHTML = "x";
   1082  var m = cm.setBookmark(Pos(0, 4), {widget: w});
   1083  w.innerHTML = "and now the widget is really really long all of a sudden and a scrollbar is needed";
   1084  m.changed();
   1085  var hScroll = byClassName(cm.getWrapperElement(), "CodeMirror-hscrollbar")[0];
   1086  is(hScroll.scrollWidth > hScroll.clientWidth);
   1087 }, {value: "abcdefg"});
   1088 
   1089 testCM("inlineWidget", function(cm) {
   1090  var w = cm.setBookmark(Pos(0, 2), {widget: document.createTextNode("uu")});
   1091  cm.setCursor(0, 2);
   1092  CodeMirror.commands.goLineDown(cm);
   1093  eqCharPos(cm.getCursor(), Pos(1, 4));
   1094  cm.setCursor(0, 2);
   1095  cm.replaceSelection("hi");
   1096  eqCharPos(w.find(), Pos(0, 2));
   1097  cm.setCursor(0, 1);
   1098  cm.replaceSelection("ay");
   1099  eqCharPos(w.find(), Pos(0, 4));
   1100  eq(cm.getLine(0), "uayuhiuu");
   1101 }, {value: "uuuu\nuuuuuu"});
   1102 
   1103 testCM("wrappingAndResizing", function(cm) {
   1104  cm.setSize(null, "auto");
   1105  cm.setOption("lineWrapping", true);
   1106  var wrap = cm.getWrapperElement(), h0 = wrap.offsetHeight;
   1107  var doc = "xxx xxx xxx xxx xxx";
   1108  cm.setValue(doc);
   1109  for (var step = 10, w = cm.charCoords(Pos(0, 18), "div").right;; w += step) {
   1110    cm.setSize(w);
   1111    if (wrap.offsetHeight <= h0 * (opera_lt10 ? 1.2 : 1.5)) {
   1112      if (step == 10) { w -= 10; step = 1; }
   1113      else break;
   1114    }
   1115  }
   1116  // Ensure that putting the cursor at the end of the maximally long
   1117  // line doesn't cause wrapping to happen.
   1118  cm.setCursor(Pos(0, doc.length));
   1119  eq(wrap.offsetHeight, h0);
   1120  cm.replaceSelection("x");
   1121  is(wrap.offsetHeight > h0, "wrapping happens");
   1122  // Now add a max-height and, in a document consisting of
   1123  // almost-wrapped lines, go over it so that a scrollbar appears.
   1124  cm.setValue(doc + "\n" + doc + "\n");
   1125  cm.getScrollerElement().style.maxHeight = "100px";
   1126  cm.replaceRange("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n!\n", Pos(2, 0));
   1127  forEach([Pos(0, doc.length), Pos(0, doc.length - 1),
   1128           Pos(0, 0), Pos(1, doc.length), Pos(1, doc.length - 1)],
   1129          function(pos) {
   1130    var coords = cm.charCoords(pos);
   1131    eqCharPos(pos, cm.coordsChar({left: coords.left + 2, top: coords.top + 5}));
   1132  });
   1133 }, null, ie_lt8);
   1134 
   1135 testCM("measureEndOfLine", function(cm) {
   1136  if (phantom) return;
   1137  cm.setSize(null, "auto");
   1138  var inner = byClassName(cm.getWrapperElement(), "CodeMirror-lines")[0].firstChild;
   1139  var lh = inner.offsetHeight;
   1140  for (var step = 10, w = cm.charCoords(Pos(0, 7), "div").right;; w += step) {
   1141    cm.setSize(w);
   1142    if (inner.offsetHeight < 2.5 * lh) {
   1143      if (step == 10) { w -= 10; step = 1; }
   1144      else break;
   1145    }
   1146  }
   1147  cm.setValue(cm.getValue() + "\n\n");
   1148  var endPos = cm.charCoords(Pos(0, 18), "local");
   1149  is(endPos.top > lh * .8, "not at top");
   1150  is(endPos.left > w - 20, "at right");
   1151  endPos = cm.charCoords(Pos(0, 18));
   1152  eqCursorPos(cm.coordsChar({left: endPos.left, top: endPos.top + 5}), Pos(0, 18, "before"));
   1153 
   1154  var wrapPos = cm.cursorCoords(Pos(0, 9, "before"));
   1155  is(wrapPos.top < endPos.top, "wrapPos is actually in first line");
   1156  eqCursorPos(cm.coordsChar({left: wrapPos.left + 10, top: wrapPos.top}), Pos(0, 9, "before"));
   1157 }, {mode: "text/html", value: "<!-- foo barrr -->", lineWrapping: true}, ie_lt8 || opera_lt10);
   1158 
   1159 testCM("measureWrappedEndOfLine", function(cm) {
   1160  if (phantom) return;
   1161  cm.setSize(null, "auto");
   1162  var inner = byClassName(cm.getWrapperElement(), "CodeMirror-lines")[0].firstChild;
   1163  var lh = inner.offsetHeight;
   1164  for (var step = 10, w = cm.charCoords(Pos(0, 7), "div").right;; w += step) {
   1165    cm.setSize(w);
   1166    if (inner.offsetHeight < 2.5 * lh) {
   1167      if (step == 10) { w -= 10; step = 1; }
   1168      else break;
   1169    }
   1170  }
   1171  for (var i = 0; i < 3; ++i) {
   1172    var endPos = cm.charCoords(Pos(0, 12)); // Next-to-last since last would wrap (#1862)
   1173    endPos.left += w; // Add width of editor just to be sure that we are behind last character
   1174    eqCursorPos(cm.coordsChar(endPos), Pos(0, 13, "before"));
   1175    endPos.left += w * 100;
   1176    eqCursorPos(cm.coordsChar(endPos), Pos(0, 13, "before"));
   1177    cm.setValue("0123456789abcابجابجابجابج");
   1178    if (i == 1) {
   1179      var node = document.createElement("div");
   1180      node.innerHTML = "hi"; node.style.height = "30px";
   1181      cm.addLineWidget(0, node, {above: true});
   1182    }
   1183  }
   1184 }, {mode: "text/html", value: "0123456789abcde0123456789", lineWrapping: true}, ie_lt8 || opera_lt10);
   1185 
   1186 testCM("measureEndOfLineBidi", function(cm) {
   1187  eqCursorPos(cm.coordsChar({left: 5000, top: cm.charCoords(Pos(0, 0)).top}), Pos(0, 8, "after"))
   1188 }, {value: "إإإإuuuuإإإإ"})
   1189 
   1190 testCM("measureWrappedBidiLevel2", function(cm) {
   1191  cm.setSize(cm.charCoords(Pos(0, 6), "editor").right + 60)
   1192  var c9 = cm.charCoords(Pos(0, 9))
   1193  eqCharPos(cm.coordsChar({left: c9.right - 1, top: c9.top + 1}), Pos(0, 9))
   1194 }, {value: "foobar إإ إإ إإ إإ 555 بببببب", lineWrapping: true})
   1195 
   1196 testCM("measureWrappedBeginOfLine", function(cm) {
   1197  if (phantom) return;
   1198  cm.setSize(null, "auto");
   1199  var inner = byClassName(cm.getWrapperElement(), "CodeMirror-lines")[0].firstChild;
   1200  var lh = inner.offsetHeight;
   1201  for (var step = 10, w = cm.charCoords(Pos(0, 7), "div").right;; w += step) {
   1202    cm.setSize(w);
   1203    if (inner.offsetHeight < 2.5 * lh) {
   1204      if (step == 10) { w -= 10; step = 1; }
   1205      else break;
   1206    }
   1207  }
   1208  var beginOfSecondLine = Pos(0, 13, "after");
   1209  for (var i = 0; i < 2; ++i) {
   1210    var beginPos = cm.charCoords(Pos(0, 0));
   1211    beginPos.left -= w;
   1212    eqCursorPos(cm.coordsChar(beginPos), Pos(0, 0, "after"));
   1213    beginPos = cm.cursorCoords(beginOfSecondLine);
   1214    beginPos.left = 0;
   1215    eqCursorPos(cm.coordsChar(beginPos), beginOfSecondLine);
   1216    cm.setValue("0123456789abcابجابجابجابج");
   1217    beginOfSecondLine = Pos(0, 25, "before");
   1218  }
   1219 }, {mode: "text/html", value: "0123456789abcde0123456789", lineWrapping: true});
   1220 
   1221 testCM("scrollVerticallyAndHorizontally", function(cm) {
   1222  if (cm.getOption("inputStyle") != "textarea") return;
   1223  cm.setSize(100, 100);
   1224  addDoc(cm, 40, 40);
   1225  cm.setCursor(39);
   1226  var wrap = cm.getWrapperElement(), bar = byClassName(wrap, "CodeMirror-vscrollbar")[0];
   1227  is(bar.offsetHeight < wrap.offsetHeight, "vertical scrollbar limited by horizontal one");
   1228  var cursorBox = byClassName(wrap, "CodeMirror-cursor")[0].getBoundingClientRect();
   1229  var editorBox = wrap.getBoundingClientRect();
   1230  is(cursorBox.bottom < editorBox.top + cm.getScrollerElement().clientHeight,
   1231     "bottom line visible");
   1232 }, {lineNumbers: true});
   1233 
   1234 testCM("moveVstuck", function(cm) {
   1235  var lines = byClassName(cm.getWrapperElement(), "CodeMirror-lines")[0].firstChild, h0 = lines.offsetHeight;
   1236  var val = "fooooooooooooooooooooooooo baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaar\n";
   1237  cm.setValue(val);
   1238  for (var w = cm.charCoords(Pos(0, 26), "div").right * 2.8;; w += 5) {
   1239    cm.setSize(w);
   1240    if (lines.offsetHeight <= 3.5 * h0) break;
   1241  }
   1242  cm.setCursor(Pos(0, val.length - 1));
   1243  cm.moveV(-1, "line");
   1244  eqCursorPos(cm.getCursor(), Pos(0, 27, "before"));
   1245  is(cm.cursorCoords(null, "local").top < h0, "cursor is in first visual line");
   1246 }, {lineWrapping: true}, ie_lt8 || opera_lt10);
   1247 
   1248 testCM("collapseOnMove", function(cm) {
   1249  cm.setSelection(Pos(0, 1), Pos(2, 4));
   1250  cm.execCommand("goLineUp");
   1251  is(!cm.somethingSelected());
   1252  eqCharPos(cm.getCursor(), Pos(0, 1));
   1253  cm.setSelection(Pos(0, 1), Pos(2, 4));
   1254  cm.execCommand("goPageDown");
   1255  is(!cm.somethingSelected());
   1256  eqCharPos(cm.getCursor(), Pos(2, 4));
   1257  cm.execCommand("goLineUp");
   1258  cm.execCommand("goLineUp");
   1259  eqCharPos(cm.getCursor(), Pos(0, 4));
   1260  cm.setSelection(Pos(0, 1), Pos(2, 4));
   1261  cm.execCommand("goCharLeft");
   1262  is(!cm.somethingSelected());
   1263  eqCharPos(cm.getCursor(), Pos(0, 1));
   1264 }, {value: "aaaaa\nb\nccccc"});
   1265 
   1266 testCM("clickTab", function(cm) {
   1267  var p0 = cm.charCoords(Pos(0, 0));
   1268  eqCharPos(cm.coordsChar({left: p0.left + 5, top: p0.top + 5}), Pos(0, 0));
   1269  eqCharPos(cm.coordsChar({left: p0.right - 5, top: p0.top + 5}), Pos(0, 1));
   1270 }, {value: "\t\n\n", lineWrapping: true, tabSize: 8});
   1271 
   1272 testCM("verticalScroll", function(cm) {
   1273  cm.setSize(100, 200);
   1274  cm.setValue("foo\nbar\nbaz\n");
   1275  var sc = cm.getScrollerElement(), baseWidth = sc.scrollWidth;
   1276  cm.replaceRange("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah", Pos(0, 0), Pos(0));
   1277  is(sc.scrollWidth > baseWidth, "scrollbar present");
   1278  cm.replaceRange("foo", Pos(0, 0), Pos(0));
   1279  if (!phantom) eq(sc.scrollWidth, baseWidth, "scrollbar gone");
   1280  cm.replaceRange("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah", Pos(0, 0), Pos(0));
   1281  cm.replaceRange("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbh", Pos(1, 0), Pos(1));
   1282  is(sc.scrollWidth > baseWidth, "present again");
   1283  var curWidth = sc.scrollWidth;
   1284  cm.replaceRange("foo", Pos(0, 0), Pos(0));
   1285  is(sc.scrollWidth < curWidth, "scrollbar smaller");
   1286  is(sc.scrollWidth > baseWidth, "but still present");
   1287 });
   1288 
   1289 testCM("extraKeys", function(cm) {
   1290  var outcome;
   1291  function fakeKey(expected, code, props) {
   1292    if (typeof code == "string") code = code.charCodeAt(0);
   1293    var e = {type: "keydown", keyCode: code, preventDefault: function(){}, stopPropagation: function(){}};
   1294    if (props) for (var n in props) e[n] = props[n];
   1295    outcome = null;
   1296    cm.triggerOnKeyDown(e);
   1297    eq(outcome, expected);
   1298  }
   1299  CodeMirror.commands.testCommand = function() {outcome = "tc";};
   1300  CodeMirror.commands.goTestCommand = function() {outcome = "gtc";};
   1301  cm.setOption("extraKeys", {"Shift-X": function() {outcome = "sx";},
   1302                             "X": function() {outcome = "x";},
   1303                             "Ctrl-Alt-U": function() {outcome = "cau";},
   1304                             "End": "testCommand",
   1305                             "Home": "goTestCommand",
   1306                             "Tab": false});
   1307  fakeKey(null, "U");
   1308  fakeKey("cau", "U", {ctrlKey: true, altKey: true});
   1309  fakeKey(null, "U", {shiftKey: true, ctrlKey: true, altKey: true});
   1310  fakeKey("x", "X");
   1311  fakeKey("sx", "X", {shiftKey: true});
   1312  fakeKey("tc", 35);
   1313  fakeKey(null, 35, {shiftKey: true});
   1314  fakeKey("gtc", 36);
   1315  fakeKey("gtc", 36, {shiftKey: true});
   1316  fakeKey(null, 9);
   1317 }, null, window.opera && mac);
   1318 
   1319 testCM("wordMovementCommands", function(cm) {
   1320  cm.execCommand("goWordLeft");
   1321  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1322  cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
   1323  eqCursorPos(cm.getCursor(), Pos(0, 7, "before"));
   1324  cm.execCommand("goWordLeft");
   1325  eqCursorPos(cm.getCursor(), Pos(0, 5, "after"));
   1326  cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
   1327  eqCursorPos(cm.getCursor(), Pos(0, 12, "before"));
   1328  cm.execCommand("goWordLeft");
   1329  eqCursorPos(cm.getCursor(), Pos(0, 9, "after"));
   1330  cm.execCommand("goWordRight"); cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
   1331  eqCursorPos(cm.getCursor(), Pos(0, 24, "before"));
   1332  cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
   1333  eqCursorPos(cm.getCursor(), Pos(1, 9, "before"));
   1334  cm.execCommand("goWordRight");
   1335  eqCursorPos(cm.getCursor(), Pos(1, 13, "before"));
   1336  cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
   1337  eqCharPos(cm.getCursor(), Pos(2, 0));
   1338 }, {value: "this is (the) firstline.\na foo12\u00e9\u00f8\u00d7bar\n"});
   1339 
   1340 testCM("groupMovementCommands", function(cm) {
   1341  cm.execCommand("goGroupLeft");
   1342  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1343  cm.execCommand("goGroupRight");
   1344  eqCursorPos(cm.getCursor(), Pos(0, 4, "before"));
   1345  cm.execCommand("goGroupRight");
   1346  eqCursorPos(cm.getCursor(), Pos(0, 7, "before"));
   1347  cm.execCommand("goGroupRight");
   1348  eqCursorPos(cm.getCursor(), Pos(0, 10, "before"));
   1349  cm.execCommand("goGroupLeft");
   1350  eqCursorPos(cm.getCursor(), Pos(0, 7, "after"));
   1351  cm.execCommand("goGroupRight"); cm.execCommand("goGroupRight"); cm.execCommand("goGroupRight");
   1352  eqCursorPos(cm.getCursor(), Pos(0, 15, "before"));
   1353  cm.setCursor(Pos(0, 17));
   1354  cm.execCommand("goGroupLeft");
   1355  eqCursorPos(cm.getCursor(), Pos(0, 16, "after"));
   1356  cm.execCommand("goGroupLeft");
   1357  eqCursorPos(cm.getCursor(), Pos(0, 14, "after"));
   1358  cm.execCommand("goGroupRight"); cm.execCommand("goGroupRight");
   1359  eqCursorPos(cm.getCursor(), Pos(0, 20, "before"));
   1360  cm.execCommand("goGroupRight");
   1361  eqCursorPos(cm.getCursor(), Pos(1, 0, "after"));
   1362  cm.execCommand("goGroupRight");
   1363  eqCursorPos(cm.getCursor(), Pos(1, 2, "before"));
   1364  cm.execCommand("goGroupRight");
   1365  eqCursorPos(cm.getCursor(), Pos(1, 5, "before"));
   1366  cm.execCommand("goGroupLeft"); cm.execCommand("goGroupLeft");
   1367  eqCursorPos(cm.getCursor(), Pos(1, 0, "after"));
   1368  cm.execCommand("goGroupLeft");
   1369  eqCursorPos(cm.getCursor(), Pos(0, 20, "after"));
   1370  cm.execCommand("goGroupLeft");
   1371  eqCursorPos(cm.getCursor(), Pos(0, 16, "after"));
   1372 }, {value: "booo ba---quux. ffff\n  abc d"});
   1373 
   1374 testCM("groupsAndWhitespace", function(cm) {
   1375  var positions = [Pos(0, 0), Pos(0, 2), Pos(0, 5), Pos(0, 9), Pos(0, 11),
   1376                   Pos(1, 0), Pos(1, 2), Pos(1, 5)];
   1377  for (var i = 1; i < positions.length; i++) {
   1378    cm.execCommand("goGroupRight");
   1379    eqCharPos(cm.getCursor(), positions[i]);
   1380  }
   1381  for (var i = positions.length - 2; i >= 0; i--) {
   1382    cm.execCommand("goGroupLeft");
   1383    eqCharPos(cm.getCursor(), i == 2 ? Pos(0, 6, "before") : positions[i]);
   1384  }
   1385 }, {value: "  foo +++  \n  bar"});
   1386 
   1387 testCM("charMovementCommands", function(cm) {
   1388  cm.execCommand("goCharLeft"); cm.execCommand("goColumnLeft");
   1389  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1390  cm.execCommand("goCharRight"); cm.execCommand("goCharRight");
   1391  eqCursorPos(cm.getCursor(), Pos(0, 2, "before"));
   1392  cm.setCursor(Pos(1, 0));
   1393  cm.execCommand("goColumnLeft");
   1394  eqCursorPos(cm.getCursor(), Pos(1, 0));
   1395  cm.execCommand("goCharLeft");
   1396  eqCursorPos(cm.getCursor(), Pos(0, 5, "before"));
   1397  cm.execCommand("goColumnRight");
   1398  eqCursorPos(cm.getCursor(), Pos(0, 5, "before"));
   1399  cm.execCommand("goCharRight");
   1400  eqCursorPos(cm.getCursor(), Pos(1, 0, "after"));
   1401  cm.execCommand("goLineEnd");
   1402  eqCursorPos(cm.getCursor(), Pos(1, 5, "before"));
   1403  cm.execCommand("goLineStartSmart");
   1404  eqCursorPos(cm.getCursor(), Pos(1, 1, "after"));
   1405  cm.execCommand("goLineStartSmart");
   1406  eqCursorPos(cm.getCursor(), Pos(1, 0, "after"));
   1407  cm.setCursor(Pos(2, 0));
   1408  cm.execCommand("goCharRight"); cm.execCommand("goColumnRight");
   1409  eqCursorPos(cm.getCursor(), Pos(2, 0));
   1410 }, {value: "line1\n ine2\n"});
   1411 
   1412 testCM("verticalMovementCommands", function(cm) {
   1413  cm.execCommand("goLineUp");
   1414  eqCharPos(cm.getCursor(), Pos(0, 0));
   1415  cm.execCommand("goLineDown");
   1416  if (!phantom) // This fails in PhantomJS, though not in a real Webkit
   1417    eqCharPos(cm.getCursor(), Pos(1, 0));
   1418  cm.setCursor(Pos(1, 12));
   1419  cm.execCommand("goLineDown");
   1420  eqCharPos(cm.getCursor(), Pos(2, 5));
   1421  cm.execCommand("goLineDown");
   1422  eqCharPos(cm.getCursor(), Pos(3, 0));
   1423  cm.execCommand("goLineUp");
   1424  eqCharPos(cm.getCursor(), Pos(2, 5));
   1425  cm.execCommand("goLineUp");
   1426  eqCharPos(cm.getCursor(), Pos(1, 12));
   1427  cm.execCommand("goPageDown");
   1428  eqCharPos(cm.getCursor(), Pos(5, 0));
   1429  cm.execCommand("goPageDown"); cm.execCommand("goLineDown");
   1430  eqCharPos(cm.getCursor(), Pos(5, 0));
   1431  cm.execCommand("goPageUp");
   1432  eqCharPos(cm.getCursor(), Pos(0, 0));
   1433 }, {value: "line1\nlong long line2\nline3\n\nline5\n"});
   1434 
   1435 testCM("verticalMovementCommandsWrapping", function(cm) {
   1436  cm.setSize(120);
   1437  cm.setCursor(Pos(0, 5));
   1438  cm.execCommand("goLineDown");
   1439  eq(cm.getCursor().line, 0);
   1440  is(cm.getCursor().ch > 5, "moved beyond wrap");
   1441  for (var i = 0; ; ++i) {
   1442    is(i < 20, "no endless loop");
   1443    cm.execCommand("goLineDown");
   1444    var cur = cm.getCursor();
   1445    if (cur.line == 1) eq(cur.ch, 5);
   1446    if (cur.line == 2) { eq(cur.ch, 1); break; }
   1447  }
   1448 }, {value: "a very long line that wraps around somehow so that we can test cursor movement\nshortone\nk",
   1449    lineWrapping: true});
   1450 
   1451 testCM("verticalMovementCommandsSingleLine", function(cm) {
   1452  cm.display.wrapper.style.height = "auto";
   1453  cm.refresh();
   1454  cm.execCommand("goLineUp");
   1455  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1456  cm.execCommand("goLineDown");
   1457  eqCursorPos(cm.getCursor(), Pos(0, 11));
   1458  cm.setCursor(Pos(0, 5));
   1459  cm.execCommand("goLineDown");
   1460  eqCursorPos(cm.getCursor(), Pos(0, 11));
   1461  cm.execCommand("goLineDown");
   1462  eqCursorPos(cm.getCursor(), Pos(0, 11));
   1463  cm.execCommand("goLineUp");
   1464  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1465  cm.execCommand("goLineUp");
   1466  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1467  cm.execCommand("goPageDown");
   1468  eqCursorPos(cm.getCursor(), Pos(0, 11));
   1469  cm.execCommand("goPageDown"); cm.execCommand("goLineDown");
   1470  eqCursorPos(cm.getCursor(), Pos(0, 11));
   1471  cm.execCommand("goPageUp");
   1472  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1473  cm.setCursor(Pos(0, 5));
   1474  cm.execCommand("goPageUp");
   1475  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1476  cm.setCursor(Pos(0, 5));
   1477  cm.execCommand("goPageDown");
   1478  eqCursorPos(cm.getCursor(), Pos(0, 11));
   1479 }, {value: "single line"});
   1480 
   1481 
   1482 testCM("rtlMovement", function(cm) {
   1483  if (cm.getOption("inputStyle") != "textarea") return;
   1484  forEach(["خحج", "خحabcخحج", "abخحخحجcd", "abخde", "abخح2342خ1حج", "خ1ح2خح3حxج",
   1485           "خحcd", "1خحcd", "abcdeح1ج", "خمرحبها مها!", "foobarر", "خ ة ق",
   1486           "<img src=\"/בדיקה3.jpg\">", "يتم السحب في 05 فبراير 2014"], function(line) {
   1487    cm.setValue(line + "\n"); cm.execCommand("goLineStart");
   1488    var cursors = byClassName(cm.getWrapperElement(), "CodeMirror-cursors")[0];
   1489    var cursor = cursors.firstChild;
   1490    var prevX = cursor.offsetLeft, prevY = cursor.offsetTop;
   1491    for (var i = 0; i <= line.length; ++i) {
   1492      cm.execCommand("goCharRight");
   1493      cursor = cursors.firstChild;
   1494      if (i == line.length) is(cursor.offsetTop > prevY, "next line");
   1495      else is(cursor.offsetLeft > prevX, "moved right");
   1496      prevX = cursor.offsetLeft; prevY = cursor.offsetTop;
   1497    }
   1498    cm.setCursor(0, 0); cm.execCommand("goLineEnd");
   1499    prevX = cursors.firstChild.offsetLeft;
   1500    for (var i = 0; i < line.length; ++i) {
   1501      cm.execCommand("goCharLeft");
   1502      cursor = cursors.firstChild;
   1503      is(cursor.offsetLeft < prevX, "moved left");
   1504      prevX = cursor.offsetLeft;
   1505    }
   1506  });
   1507 }, null, ie_lt9);
   1508 
   1509 // Verify that updating a line clears its bidi ordering
   1510 testCM("bidiUpdate", function(cm) {
   1511  cm.setCursor(Pos(0, 2, "before"));
   1512  cm.replaceSelection("خحج", "start");
   1513  cm.execCommand("goCharRight");
   1514  eqCursorPos(cm.getCursor(), Pos(0, 6, "before"));
   1515 }, {value: "abcd\n"});
   1516 
   1517 testCM("movebyTextUnit", function(cm) {
   1518  cm.setValue("בְּרֵאשִ\nééé́\n");
   1519  cm.execCommand("goLineStart");
   1520  for (var i = 0; i < 4; ++i) cm.execCommand("goCharRight");
   1521  eqCursorPos(cm.getCursor(), Pos(0, 0, "after"));
   1522  cm.execCommand("goCharRight");
   1523  eqCursorPos(cm.getCursor(), Pos(1, 0, "after"));
   1524  cm.execCommand("goCharRight");
   1525  cm.execCommand("goCharRight");
   1526  eqCursorPos(cm.getCursor(), Pos(1, 4, "before"));
   1527  cm.execCommand("goCharRight");
   1528  eqCursorPos(cm.getCursor(), Pos(1, 7, "before"));
   1529 });
   1530 
   1531 testCM("lineChangeEvents", function(cm) {
   1532  addDoc(cm, 3, 5);
   1533  var log = [], want = ["ch 0", "ch 1", "del 2", "ch 0", "ch 0", "del 1", "del 3", "del 4"];
   1534  for (var i = 0; i < 5; ++i) {
   1535    CodeMirror.on(cm.getLineHandle(i), "delete", function(i) {
   1536      return function() {log.push("del " + i);};
   1537    }(i));
   1538    CodeMirror.on(cm.getLineHandle(i), "change", function(i) {
   1539      return function() {log.push("ch " + i);};
   1540    }(i));
   1541  }
   1542  cm.replaceRange("x", Pos(0, 1));
   1543  cm.replaceRange("xy", Pos(1, 1), Pos(2));
   1544  cm.replaceRange("foo\nbar", Pos(0, 1));
   1545  cm.replaceRange("", Pos(0, 0), Pos(cm.lineCount()));
   1546  eq(log.length, want.length, "same length");
   1547  for (var i = 0; i < log.length; ++i)
   1548    eq(log[i], want[i]);
   1549 });
   1550 
   1551 testCM("scrollEntirelyToRight", function(cm) {
   1552  if (phantom || cm.getOption("inputStyle") != "textarea") return;
   1553  addDoc(cm, 500, 2);
   1554  cm.setCursor(Pos(0, 500));
   1555  var wrap = cm.getWrapperElement(), cur = byClassName(wrap, "CodeMirror-cursor")[0];
   1556  is(wrap.getBoundingClientRect().right > cur.getBoundingClientRect().left);
   1557 });
   1558 
   1559 testCM("lineWidgets", function(cm) {
   1560  addDoc(cm, 500, 3);
   1561  var last = cm.charCoords(Pos(2, 0));
   1562  var node = document.createElement("div");
   1563  node.innerHTML = "hi";
   1564  var widget = cm.addLineWidget(1, node);
   1565  is(last.top < cm.charCoords(Pos(2, 0)).top, "took up space");
   1566  cm.setCursor(Pos(1, 1));
   1567  cm.execCommand("goLineDown");
   1568  eqCharPos(cm.getCursor(), Pos(2, 1));
   1569  cm.execCommand("goLineUp");
   1570  eqCharPos(cm.getCursor(), Pos(1, 1));
   1571 });
   1572 
   1573 testCM("lineWidgetFocus", function(cm) {
   1574  var place = document.getElementById("testground");
   1575  place.className = "offscreen";
   1576  try {
   1577    addDoc(cm, 500, 10);
   1578    var node = document.createElement("input");
   1579    var widget = cm.addLineWidget(1, node);
   1580    node.focus();
   1581    eq(document.activeElement, node);
   1582    cm.replaceRange("new stuff", Pos(1, 0));
   1583    eq(document.activeElement, node);
   1584  } finally {
   1585    place.className = "";
   1586  }
   1587 });
   1588 
   1589 testCM("lineWidgetCautiousRedraw", function(cm) {
   1590  var node = document.createElement("div");
   1591  node.innerHTML = "hahah";
   1592  var w = cm.addLineWidget(0, node);
   1593  var redrawn = false;
   1594  w.on("redraw", function() { redrawn = true; });
   1595  cm.replaceSelection("0");
   1596  is(!redrawn);
   1597 }, {value: "123\n456"});
   1598 
   1599 
   1600 var knownScrollbarWidth;
   1601 function scrollbarWidth(measure) {
   1602  if (knownScrollbarWidth != null) return knownScrollbarWidth;
   1603  var div = document.createElement('div');
   1604  div.style.cssText = "width: 50px; height: 50px; overflow-x: scroll";
   1605  document.body.appendChild(div);
   1606  knownScrollbarWidth = div.offsetHeight - div.clientHeight;
   1607  document.body.removeChild(div);
   1608  return knownScrollbarWidth || 0;
   1609 }
   1610 
   1611 testCM("lineWidgetChanged", function(cm) {
   1612  addDoc(cm, 2, 300);
   1613  var halfScrollbarWidth = scrollbarWidth(cm.display.measure)/2;
   1614  cm.setOption('lineNumbers', true);
   1615  cm.setSize(600, cm.defaultTextHeight() * 50);
   1616  cm.scrollTo(null, cm.heightAtLine(125, "local"));
   1617 
   1618  var expectedWidgetHeight = 60;
   1619  var expectedLinesInWidget = 3;
   1620  function w() {
   1621    var node = document.createElement("div");
   1622    // we use these children with just under half width of the line to check measurements are made with correct width
   1623    // when placed in the measure div.
   1624    // If the widget is measured at a width much narrower than it is displayed at, the underHalf children will span two lines and break the test.
   1625    // If the widget is measured at a width much wider than it is displayed at, the overHalf children will combine and break the test.
   1626    // Note that this test only checks widgets where coverGutter is true, because these require extra styling to get the width right.
   1627    // It may also be worthwhile to check this for non-coverGutter widgets.
   1628    // Visually:
   1629    // Good:
   1630    // | ------------- display width ------------- |
   1631    // | ------- widget-width when measured ------ |
   1632    // | | -- under-half -- | | -- under-half -- | |
   1633    // | | --- over-half --- |                     |
   1634    // | | --- over-half --- |                     |
   1635    // Height: measured as 3 lines, same as it will be when actually displayed
   1636 
   1637    // Bad (too narrow):
   1638    // | ------------- display width ------------- |
   1639    // | ------ widget-width when measured ----- |  < -- uh oh
   1640    // | | -- under-half -- |                    |
   1641    // | | -- under-half -- |                    |  < -- when measured, shoved to next line
   1642    // | | --- over-half --- |                   |
   1643    // | | --- over-half --- |                   |
   1644    // Height: measured as 4 lines, more than expected . Will be displayed as 3 lines!
   1645 
   1646    // Bad (too wide):
   1647    // | ------------- display width ------------- |
   1648    // | -------- widget-width when measured ------- | < -- uh oh
   1649    // | | -- under-half -- | | -- under-half -- |   |
   1650    // | | --- over-half --- | | --- over-half --- | | < -- when measured, combined on one line
   1651    // Height: measured as 2 lines, less than expected. Will be displayed as 3 lines!
   1652 
   1653    var barelyUnderHalfWidthHtml = '<div style="display: inline-block; height: 1px; width: '+(285 - halfScrollbarWidth)+'px;"></div>';
   1654    var barelyOverHalfWidthHtml = '<div style="display: inline-block; height: 1px; width: '+(305 - halfScrollbarWidth)+'px;"></div>';
   1655    node.innerHTML = new Array(3).join(barelyUnderHalfWidthHtml) + new Array(3).join(barelyOverHalfWidthHtml);
   1656    node.style.cssText = "background: yellow;font-size:0;line-height: " + (expectedWidgetHeight/expectedLinesInWidget) + "px;";
   1657    return node;
   1658  }
   1659  var info0 = cm.getScrollInfo();
   1660  var w0 = cm.addLineWidget(0, w(), { coverGutter: true });
   1661  var w150 = cm.addLineWidget(150, w(), { coverGutter: true });
   1662  var w300 = cm.addLineWidget(300, w(), { coverGutter: true });
   1663  var info1 = cm.getScrollInfo();
   1664  eq(info0.height + (3 * expectedWidgetHeight), info1.height);
   1665  eq(info0.top + expectedWidgetHeight, info1.top);
   1666  expectedWidgetHeight = 12;
   1667  w0.node.style.lineHeight = w150.node.style.lineHeight = w300.node.style.lineHeight = (expectedWidgetHeight/expectedLinesInWidget) + "px";
   1668  w0.changed(); w150.changed(); w300.changed();
   1669  var info2 = cm.getScrollInfo();
   1670  eq(info0.height + (3 * expectedWidgetHeight), info2.height);
   1671  eq(info0.top + expectedWidgetHeight, info2.top);
   1672 });
   1673 
   1674 testCM("lineWidgetIssue5486", function(cm) {
   1675  // [prepare]
   1676  // 2nd line is combined to 1st line due to markText
   1677  // 2nd line has a lineWidget below
   1678 
   1679  cm.setValue("Lorem\nIpsue\nDollar")
   1680 
   1681  var el = document.createElement('div')
   1682  el.style.height='50px'
   1683  el.textContent = '[[LINE WIDGET]]'
   1684  
   1685  var lineWidget = cm.addLineWidget(1, el, {
   1686    above: false,
   1687    coverGutter: false,
   1688    noHScroll: false,
   1689    showIfHidden: false,
   1690  })
   1691  
   1692  var marker = document.createElement('span')
   1693  marker.textContent = '[--]'
   1694  
   1695  cm.markText({line:0, ch: 1}, {line:1, ch: 4}, {
   1696    replacedWith: marker
   1697  })
   1698 
   1699  // before resizing the lineWidget, measure 3rd line position
   1700 
   1701  var measure_1 = Math.round(cm.charCoords({line:2, ch:0}).top)
   1702  
   1703  // resize lineWidget, height + 50 px
   1704 
   1705  el.style.height='100px'
   1706  el.textContent += "\nlineWidget size changed.\nTry moving cursor to line 3?"
   1707  
   1708  lineWidget.changed()
   1709 
   1710  // re-measure 3rd line position
   1711  var measure_2 = Math.round(cm.charCoords({line:2, ch:0}).top)
   1712  eq(measure_2, measure_1 + 50)
   1713 
   1714  // (extra test)
   1715  //
   1716  // add char to the right of the folded marker
   1717  // and re-measure 3rd line position
   1718 
   1719  cm.replaceRange('-', {line:1, ch: 5})
   1720  var measure_3 = Math.round(cm.charCoords({line:2, ch:0}).top)
   1721  eq(measure_3, measure_2)
   1722 });
   1723 
   1724 testCM("getLineNumber", function(cm) {
   1725  addDoc(cm, 2, 20);
   1726  var h1 = cm.getLineHandle(1);
   1727  eq(cm.getLineNumber(h1), 1);
   1728  cm.replaceRange("hi\nbye\n", Pos(0, 0));
   1729  eq(cm.getLineNumber(h1), 3);
   1730  cm.setValue("");
   1731  eq(cm.getLineNumber(h1), null);
   1732 });
   1733 
   1734 testCM("jumpTheGap", function(cm) {
   1735  if (phantom) return;
   1736  var longLine = "abcdef ghiklmnop qrstuvw xyz ";
   1737  longLine += longLine; longLine += longLine; longLine += longLine;
   1738  cm.replaceRange(longLine, Pos(2, 0), Pos(2));
   1739  cm.setSize("200px", null);
   1740  cm.getWrapperElement().style.lineHeight = 2;
   1741  cm.refresh();
   1742  cm.setCursor(Pos(0, 1));
   1743  cm.execCommand("goLineDown");
   1744  eqCharPos(cm.getCursor(), Pos(1, 1));
   1745  cm.execCommand("goLineDown");
   1746  eqCharPos(cm.getCursor(), Pos(2, 1));
   1747  cm.execCommand("goLineDown");
   1748  eq(cm.getCursor().line, 2);
   1749  is(cm.getCursor().ch > 1);
   1750  cm.execCommand("goLineUp");
   1751  eqCharPos(cm.getCursor(), Pos(2, 1));
   1752  cm.execCommand("goLineUp");
   1753  eqCharPos(cm.getCursor(), Pos(1, 1));
   1754  var node = document.createElement("div");
   1755  node.innerHTML = "hi"; node.style.height = "30px";
   1756  cm.addLineWidget(0, node);
   1757  cm.addLineWidget(1, node.cloneNode(true), {above: true});
   1758  cm.setCursor(Pos(0, 2));
   1759  cm.execCommand("goLineDown");
   1760  eqCharPos(cm.getCursor(), Pos(1, 2));
   1761  cm.execCommand("goLineUp");
   1762  eqCharPos(cm.getCursor(), Pos(0, 2));
   1763 }, {lineWrapping: true, value: "abc\ndef\nghi\njkl\n"});
   1764 
   1765 testCM("addLineClass", function(cm) {
   1766  function cls(line, text, bg, wrap, gutter) {
   1767    var i = cm.lineInfo(line);
   1768    eq(i.textClass, text);
   1769    eq(i.bgClass, bg);
   1770    eq(i.wrapClass, wrap);
   1771    if (typeof i.handle.gutterClass !== 'undefined') {
   1772        eq(i.handle.gutterClass, gutter);
   1773    }
   1774  }
   1775  cm.addLineClass(0, "text", "foo");
   1776  cm.addLineClass(0, "text", "bar");
   1777  cm.addLineClass(1, "background", "baz");
   1778  cm.addLineClass(1, "wrap", "foo");
   1779  cm.addLineClass(1, "gutter", "gutter-class");
   1780  cls(0, "foo bar", null, null, null);
   1781  cls(1, null, "baz", "foo", "gutter-class");
   1782  var lines = cm.display.lineDiv;
   1783  eq(byClassName(lines, "foo").length, 2);
   1784  eq(byClassName(lines, "bar").length, 1);
   1785  eq(byClassName(lines, "baz").length, 1);
   1786  eq(byClassName(lines, "gutter-class").length, 2); // Gutter classes are reflected in 2 nodes
   1787  cm.removeLineClass(0, "text", "foo");
   1788  cls(0, "bar", null, null, null);
   1789  cm.removeLineClass(0, "text", "foo");
   1790  cls(0, "bar", null, null, null);
   1791  cm.removeLineClass(0, "text", "bar");
   1792  cls(0, null, null, null);
   1793 
   1794  cm.addLineClass(1, "wrap", "quux");
   1795  cls(1, null, "baz", "foo quux", "gutter-class");
   1796  cm.removeLineClass(1, "wrap");
   1797  cls(1, null, "baz", null, "gutter-class");
   1798  cm.removeLineClass(1, "gutter", "gutter-class");
   1799  eq(byClassName(lines, "gutter-class").length, 0);
   1800  cls(1, null, "baz", null, null);
   1801 
   1802  cm.addLineClass(1, "gutter", "gutter-class");
   1803  cls(1, null, "baz", null, "gutter-class");
   1804  cm.removeLineClass(1, "gutter", "gutter-class");
   1805  cls(1, null, "baz", null, null);
   1806 
   1807 }, {value: "hohoho\n", lineNumbers: true});
   1808 
   1809 testCM("atomicMarker", function(cm) {
   1810  addDoc(cm, 10, 10);
   1811 
   1812  function atom(ll, cl, lr, cr, li, ri, ls, rs) {
   1813    var options = {
   1814      atomic: true,
   1815      inclusiveLeft: li,
   1816      inclusiveRight: ri
   1817    };
   1818 
   1819    if (ls === true || ls === false) options["selectLeft"] = ls;
   1820    if (rs === true || rs === false) options["selectRight"] = rs;
   1821 
   1822    return cm.markText(Pos(ll, cl), Pos(lr, cr), options);
   1823  }
   1824 
   1825  // Can cursor to the left and right of a normal marker by jumping across it
   1826  var m = atom(0, 1, 0, 5);
   1827  cm.setCursor(Pos(0, 1));
   1828  cm.execCommand("goCharRight");
   1829  eqCursorPos(cm.getCursor(), Pos(0, 5));
   1830  cm.execCommand("goCharLeft");
   1831  eqCursorPos(cm.getCursor(), Pos(0, 1));
   1832  m.clear();
   1833 
   1834  // Can't cursor to the left of a marker when inclusiveLeft=true
   1835  m = atom(0, 0, 0, 5, true);
   1836  eqCursorPos(cm.getCursor(), Pos(0, 5), "pushed out");
   1837  cm.execCommand("goCharLeft");
   1838  eqCursorPos(cm.getCursor(), Pos(0, 5));
   1839  m.clear();
   1840 
   1841  // Can't cursor to the left of a marker when inclusiveLeft=false and selectLeft=false
   1842  m = atom(0, 0, 0, 5, false, false, false);
   1843  cm.setCursor(Pos(0, 5));
   1844  eqCursorPos(cm.getCursor(), Pos(0, 5), "pushed out");
   1845  cm.execCommand("goCharLeft");
   1846  eqCursorPos(cm.getCursor(), Pos(0, 5));
   1847  m.clear();
   1848 
   1849  // Can cursor to the left of a marker when inclusiveLeft=false and selectLeft=True
   1850  m = atom(0, 0, 0, 5, false, false, true);
   1851  cm.setCursor(Pos(0, 5));
   1852  eqCursorPos(cm.getCursor(), Pos(0, 5), "pushed out");
   1853  cm.execCommand("goCharLeft");
   1854  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1855  m.clear();
   1856 
   1857  // Can't cursor to the right of a marker when inclusiveRight=true
   1858  m = atom(0, 0, 0, 5, false, true);
   1859  cm.setCursor(Pos(0, 0));
   1860  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1861  cm.execCommand("goCharRight");
   1862  eqCursorPos(cm.getCursor(), Pos(0, 6));
   1863  m.clear();
   1864 
   1865  // Can't cursor to the right of a marker when inclusiveRight=false and selectRight=false
   1866  m = atom(0, 0, 0, 5, false, false, true, false);
   1867  cm.setCursor(Pos(0, 0));
   1868  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1869  cm.execCommand("goCharRight");
   1870  eqCursorPos(cm.getCursor(), Pos(0, 6));
   1871  m.clear();
   1872 
   1873  // Can cursor to the right of a marker when inclusiveRight=false and selectRight=True
   1874  m = atom(0, 0, 0, 5, false, false, true, true);
   1875  cm.setCursor(Pos(0, 0));
   1876  eqCursorPos(cm.getCursor(), Pos(0, 0));
   1877  cm.execCommand("goCharRight");
   1878  eqCursorPos(cm.getCursor(), Pos(0, 5));
   1879  m.clear();
   1880 
   1881  // Can't cursor to the right of a multiline marker when inclusiveRight=true
   1882  m = atom(8, 4, 9, 10, false, true);
   1883  cm.setCursor(Pos(9, 8));
   1884  eqCursorPos(cm.getCursor(), Pos(8, 4), "set");
   1885  cm.execCommand("goCharRight");
   1886  eqCursorPos(cm.getCursor(), Pos(8, 4), "char right");
   1887  cm.execCommand("goLineDown");
   1888  eqCursorPos(cm.getCursor(), Pos(8, 4), "line down");
   1889  cm.execCommand("goCharLeft");
   1890  eqCursorPos(cm.getCursor(), Pos(8, 3, "after"));
   1891  m.clear();
   1892 
   1893  // Cursor jumps across a multiline atomic marker,
   1894  // and backspace deletes the entire marker
   1895  m = atom(1, 1, 3, 8);
   1896  cm.setCursor(Pos(0, 0));
   1897  cm.setCursor(Pos(2, 0));
   1898  eqCursorPos(cm.getCursor(), Pos(3, 8));
   1899  cm.execCommand("goCharLeft");
   1900  eqCursorPos(cm.getCursor(), Pos(1, 1));
   1901  cm.execCommand("goCharRight");
   1902  eqCursorPos(cm.getCursor(), Pos(3, 8));
   1903  cm.execCommand("goLineUp");
   1904  eqCursorPos(cm.getCursor(), Pos(1, 1));
   1905  cm.execCommand("goLineDown");
   1906  eqCursorPos(cm.getCursor(), Pos(3, 8));
   1907  cm.execCommand("delCharBefore");
   1908  eq(cm.getValue().length, 80, "del chunk");
   1909  m.clear();
   1910  addDoc(cm, 10, 10);
   1911 
   1912  // Delete before an atomic marker deletes the entire marker
   1913  m = atom(3, 0, 5, 5);
   1914  cm.setCursor(Pos(3, 0));
   1915  cm.execCommand("delWordAfter");
   1916  eq(cm.getValue().length, 82, "del chunk");
   1917  m.clear();
   1918  addDoc(cm, 10, 10);
   1919 });
   1920 
   1921 testCM("selectionBias", function(cm) {
   1922  cm.markText(Pos(0, 1), Pos(0, 3), {atomic: true});
   1923  cm.setCursor(Pos(0, 2));
   1924  eqCursorPos(cm.getCursor(), Pos(0, 1));
   1925  cm.setCursor(Pos(0, 2));
   1926  eqCursorPos(cm.getCursor(), Pos(0, 3));
   1927  cm.setCursor(Pos(0, 2));
   1928  eqCursorPos(cm.getCursor(), Pos(0, 1));
   1929  cm.setCursor(Pos(0, 2), null, {bias: -1});
   1930  eqCursorPos(cm.getCursor(), Pos(0, 1));
   1931  cm.setCursor(Pos(0, 4));
   1932  cm.setCursor(Pos(0, 2), null, {bias: 1});
   1933  eqCursorPos(cm.getCursor(), Pos(0, 3));
   1934 }, {value: "12345"});
   1935 
   1936 testCM("selectionHomeEnd", function(cm) {
   1937  cm.markText(Pos(1, 0), Pos(1, 1), {atomic: true, inclusiveLeft: true});
   1938  cm.markText(Pos(1, 3), Pos(1, 4), {atomic: true, inclusiveRight: true});
   1939  cm.setCursor(Pos(1, 2));
   1940  cm.execCommand("goLineStart");
   1941  eqCursorPos(cm.getCursor(), Pos(1, 1));
   1942  cm.execCommand("goLineEnd");
   1943  eqCursorPos(cm.getCursor(), Pos(1, 3));
   1944 }, {value: "ab\ncdef\ngh"});
   1945 
   1946 testCM("readOnlyMarker", function(cm) {
   1947  function mark(ll, cl, lr, cr, at) {
   1948    return cm.markText(Pos(ll, cl), Pos(lr, cr),
   1949                       {readOnly: true, atomic: at});
   1950  }
   1951  var m = mark(0, 1, 0, 4);
   1952  cm.setCursor(Pos(0, 2));
   1953  cm.replaceSelection("hi", "end");
   1954  eqCursorPos(cm.getCursor(), Pos(0, 2));
   1955  eq(cm.getLine(0), "abcde");
   1956  cm.execCommand("selectAll");
   1957  cm.replaceSelection("oops", "around");
   1958  eq(cm.getValue(), "oopsbcd");
   1959  cm.undo();
   1960  eqCursorPos(m.find().from, Pos(0, 1));
   1961  eqCursorPos(m.find().to, Pos(0, 4));
   1962  m.clear();
   1963  cm.setCursor(Pos(0, 2));
   1964  cm.replaceSelection("hi", "around");
   1965  eq(cm.getLine(0), "abhicde");
   1966  eqCursorPos(cm.getCursor(), Pos(0, 4));
   1967  m = mark(0, 2, 2, 2, true);
   1968  cm.setSelection(Pos(1, 1), Pos(2, 4));
   1969  cm.replaceSelection("t", "end");
   1970  eqCursorPos(cm.getCursor(), Pos(2, 3));
   1971  eq(cm.getLine(2), "klto");
   1972  cm.execCommand("goCharLeft");
   1973  cm.execCommand("goCharLeft");
   1974  eqCursorPos(cm.getCursor(), Pos(0, 2));
   1975  cm.setSelection(Pos(0, 1), Pos(0, 3));
   1976  cm.replaceSelection("xx", "around");
   1977  eqCursorPos(cm.getCursor(), Pos(0, 3));
   1978  eq(cm.getLine(0), "axxhicde");
   1979 }, {value: "abcde\nfghij\nklmno\n"});
   1980 
   1981 testCM("dirtyBit", function(cm) {
   1982  eq(cm.isClean(), true);
   1983  cm.replaceSelection("boo", null, "test");
   1984  eq(cm.isClean(), false);
   1985  cm.undo();
   1986  eq(cm.isClean(), true);
   1987  cm.replaceSelection("boo", null, "test");
   1988  cm.replaceSelection("baz", null, "test");
   1989  cm.undo();
   1990  eq(cm.isClean(), false);
   1991  cm.markClean();
   1992  eq(cm.isClean(), true);
   1993  cm.undo();
   1994  eq(cm.isClean(), false);
   1995  cm.redo();
   1996  eq(cm.isClean(), true);
   1997 });
   1998 
   1999 testCM("changeGeneration", function(cm) {
   2000  cm.replaceSelection("x");
   2001  var softGen = cm.changeGeneration();
   2002  cm.replaceSelection("x");
   2003  cm.undo();
   2004  eq(cm.getValue(), "");
   2005  is(!cm.isClean(softGen));
   2006  cm.replaceSelection("x");
   2007  var hardGen = cm.changeGeneration(true);
   2008  cm.replaceSelection("x");
   2009  cm.undo();
   2010  eq(cm.getValue(), "x");
   2011  is(cm.isClean(hardGen));
   2012 });
   2013 
   2014 testCM("addKeyMap", function(cm) {
   2015  function sendKey(code) {
   2016    cm.triggerOnKeyDown({type: "keydown", keyCode: code,
   2017                         preventDefault: function(){}, stopPropagation: function(){}});
   2018  }
   2019 
   2020  sendKey(39);
   2021  eqCursorPos(cm.getCursor(), Pos(0, 1, "before"));
   2022  var test = 0;
   2023  var map1 = {Right: function() { ++test; }}, map2 = {Right: function() { test += 10; }}
   2024  cm.addKeyMap(map1);
   2025  sendKey(39);
   2026  eqCursorPos(cm.getCursor(), Pos(0, 1, "before"));
   2027  eq(test, 1);
   2028  cm.addKeyMap(map2, true);
   2029  sendKey(39);
   2030  eq(test, 2);
   2031  cm.removeKeyMap(map1);
   2032  sendKey(39);
   2033  eq(test, 12);
   2034  cm.removeKeyMap(map2);
   2035  sendKey(39);
   2036  eq(test, 12);
   2037  eqCursorPos(cm.getCursor(), Pos(0, 2, "before"));
   2038  cm.addKeyMap({Right: function() { test = 55; }, name: "mymap"});
   2039  sendKey(39);
   2040  eq(test, 55);
   2041  cm.removeKeyMap("mymap");
   2042  sendKey(39);
   2043  eqCursorPos(cm.getCursor(), Pos(0, 3, "before"));
   2044 }, {value: "abc"});
   2045 
   2046 function mouseDown(cm, button, pos, mods) {
   2047  var coords = cm.charCoords(pos, "window")
   2048  var event = {type: "mousedown",
   2049               preventDefault: Math.min,
   2050               which: button,
   2051               target: cm.display.lineDiv,
   2052               clientX: coords.left, clientY: coords.top}
   2053  if (mods) for (var prop in mods) event[prop] = mods[prop]
   2054  cm.triggerOnMouseDown(event)
   2055 }
   2056 
   2057 testCM("mouseBinding", function(cm) {
   2058  var fired = []
   2059  cm.addKeyMap({
   2060    "Shift-LeftClick": function(_cm, pos) {
   2061      eqCharPos(pos, Pos(1, 2))
   2062      fired.push("a")
   2063    },
   2064    "Shift-LeftDoubleClick": function() { fired.push("b") },
   2065    "Shift-LeftTripleClick": function() { fired.push("c") }
   2066  })
   2067 
   2068  function send(button, mods) { mouseDown(cm, button, Pos(1, 2), mods) }
   2069  send(1, {shiftKey: true})
   2070  send(1, {shiftKey: true})
   2071  send(1, {shiftKey: true})
   2072  send(1, {})
   2073  send(2, {ctrlKey: true})
   2074  send(2, {ctrlKey: true})
   2075  eq(fired.join(" "), "a b c")
   2076 }, {value: "foo\nbar\nbaz"})
   2077 
   2078 testCM("configureMouse", function(cm) {
   2079  cm.setOption("configureMouse", function() { return {unit: "word"} })
   2080  mouseDown(cm, 1, Pos(0, 5))
   2081  eqCharPos(cm.getCursor("from"), Pos(0, 4))
   2082  eqCharPos(cm.getCursor("to"), Pos(0, 7))
   2083  cm.setOption("configureMouse", function() { return {extend: true} })
   2084  mouseDown(cm, 1, Pos(0, 0))
   2085  eqCharPos(cm.getCursor("from"), Pos(0, 0))
   2086  eqCharPos(cm.getCursor("to"), Pos(0, 4))
   2087 }, {value: "foo bar baz"})
   2088 
   2089 testCM("findPosH", function(cm) {
   2090  forEach([{from: Pos(0, 0), to: Pos(0, 1, "before"), by: 1},
   2091           {from: Pos(0, 0), to: Pos(0, 0), by: -1, hitSide: true},
   2092           {from: Pos(0, 0), to: Pos(0, 4, "before"), by: 1, unit: "word"},
   2093           {from: Pos(0, 0), to: Pos(0, 8, "before"), by: 2, unit: "word"},
   2094           {from: Pos(0, 0), to: Pos(2, 0, "after"), by: 20, unit: "word", hitSide: true},
   2095           {from: Pos(0, 7), to: Pos(0, 5, "after"), by: -1, unit: "word"},
   2096           {from: Pos(0, 4), to: Pos(0, 8, "before"), by: 1, unit: "word"},
   2097           {from: Pos(1, 0), to: Pos(1, 18, "before"), by: 3, unit: "word"},
   2098           {from: Pos(1, 22), to: Pos(1, 5, "after"), by: -3, unit: "word"},
   2099           {from: Pos(1, 15), to: Pos(1, 10, "after"), by: -5},
   2100           {from: Pos(1, 15), to: Pos(1, 10, "after"), by: -5, unit: "column"},
   2101           {from: Pos(1, 15), to: Pos(1, 0, "after"), by: -50, unit: "column", hitSide: true},
   2102           {from: Pos(1, 15), to: Pos(1, 24, "before"), by: 50, unit: "column", hitSide: true},
   2103           {from: Pos(1, 15), to: Pos(2, 0, "after"), by: 50, hitSide: true}], function(t) {
   2104    var r = cm.findPosH(t.from, t.by, t.unit || "char");
   2105    eqCursorPos(r, t.to);
   2106    eq(!!r.hitSide, !!t.hitSide);
   2107  });
   2108 }, {value: "line one\nline two.something.other\n"});
   2109 
   2110 testCM("beforeChange", function(cm) {
   2111  cm.on("beforeChange", function(cm, change) {
   2112    var text = [];
   2113    for (var i = 0; i < change.text.length; ++i)
   2114      text.push(change.text[i].replace(/\s/g, "_"));
   2115    change.update(null, null, text);
   2116  });
   2117  cm.setValue("hello, i am a\nnew document\n");
   2118  eq(cm.getValue(), "hello,_i_am_a\nnew_document\n");
   2119  CodeMirror.on(cm.getDoc(), "beforeChange", function(doc, change) {
   2120    if (change.from.line == 0) change.cancel();
   2121  });
   2122  cm.setValue("oops"); // Canceled
   2123  eq(cm.getValue(), "hello,_i_am_a\nnew_document\n");
   2124  cm.replaceRange("hey hey hey", Pos(1, 0), Pos(2, 0));
   2125  eq(cm.getValue(), "hello,_i_am_a\nhey_hey_hey");
   2126 }, {value: "abcdefghijk"});
   2127 
   2128 testCM("beforeChangeUndo", function(cm) {
   2129  cm.replaceRange("hi", Pos(0, 0), Pos(0));
   2130  cm.replaceRange("bye", Pos(0, 0), Pos(0));
   2131  eq(cm.historySize().undo, 2);
   2132  cm.on("beforeChange", function(cm, change) {
   2133    is(!change.update);
   2134    change.cancel();
   2135  });
   2136  cm.undo();
   2137  eq(cm.historySize().undo, 0);
   2138  eq(cm.getValue(), "bye\ntwo");
   2139 }, {value: "one\ntwo"});
   2140 
   2141 testCM("beforeSelectionChange", function(cm) {
   2142  function notAtEnd(cm, pos) {
   2143    var len = cm.getLine(pos.line).length;
   2144    if (!len || pos.ch == len) return Pos(pos.line, pos.ch - 1);
   2145    return pos;
   2146  }
   2147  cm.on("beforeSelectionChange", function(cm, obj) {
   2148    obj.update([{anchor: notAtEnd(cm, obj.ranges[0].anchor),
   2149                 head: notAtEnd(cm, obj.ranges[0].head)}]);
   2150  });
   2151 
   2152  addDoc(cm, 10, 10);
   2153  cm.execCommand("goLineEnd");
   2154  eqCursorPos(cm.getCursor(), Pos(0, 9));
   2155  cm.execCommand("selectAll");
   2156  eqCursorPos(cm.getCursor("start"), Pos(0, 0));
   2157  eqCursorPos(cm.getCursor("end"), Pos(9, 9));
   2158 });
   2159 
   2160 testCM("change_removedText", function(cm) {
   2161  cm.setValue("abc\ndef");
   2162 
   2163  var removedText = [];
   2164  cm.on("change", function(cm, change) {
   2165    removedText.push(change.removed);
   2166  });
   2167 
   2168  cm.operation(function() {
   2169    cm.replaceRange("xyz", Pos(0, 0), Pos(1,1));
   2170    cm.replaceRange("123", Pos(0,0));
   2171  });
   2172 
   2173  eq(removedText.length, 2);
   2174  eq(removedText[0].join("\n"), "abc\nd");
   2175  eq(removedText[1].join("\n"), "");
   2176 
   2177  var removedText = [];
   2178  cm.undo();
   2179  eq(removedText.length, 2);
   2180  eq(removedText[0].join("\n"), "123");
   2181  eq(removedText[1].join("\n"), "xyz");
   2182 
   2183  var removedText = [];
   2184  cm.redo();
   2185  eq(removedText.length, 2);
   2186  eq(removedText[0].join("\n"), "abc\nd");
   2187  eq(removedText[1].join("\n"), "");
   2188 });
   2189 
   2190 testCM("lineStyleFromMode", function(cm) {
   2191  CodeMirror.defineMode("test_mode", function() {
   2192    return {token: function(stream) {
   2193      if (stream.match(/^\[[^\]]*\]/)) return "  line-brackets  ";
   2194      if (stream.match(/^\([^\)]*\)/)) return "  line-background-parens  ";
   2195      if (stream.match(/^<[^>]*>/)) return "  span  line-line  line-background-bg  ";
   2196      stream.match(/^\s+|^\S+/);
   2197    }};
   2198  });
   2199  cm.setOption("mode", "test_mode");
   2200  var bracketElts = byClassName(cm.getWrapperElement(), "brackets");
   2201  eq(bracketElts.length, 1, "brackets count");
   2202  eq(bracketElts[0].nodeName, "PRE");
   2203  is(!/brackets.*brackets/.test(bracketElts[0].className));
   2204  var parenElts = byClassName(cm.getWrapperElement(), "parens");
   2205  eq(parenElts.length, 1, "parens count");
   2206  eq(parenElts[0].nodeName, "DIV");
   2207  is(!/parens.*parens/.test(parenElts[0].className));
   2208  eq(parenElts[0].parentElement.nodeName, "DIV");
   2209 
   2210  is(byClassName(cm.getWrapperElement(), "bg").length > 0);
   2211  is(byClassName(cm.getWrapperElement(), "line").length > 0);
   2212  var spanElts = byClassName(cm.getWrapperElement(), "cm-span");
   2213  eq(spanElts.length, 2);
   2214  is(/^\s*cm-span\s*$/.test(spanElts[0].className));
   2215 }, {value: "line1: [br] [br]\nline2: (par) (par)\nline3: <tag> <tag>"});
   2216 
   2217 testCM("lineStyleFromBlankLine", function(cm) {
   2218  CodeMirror.defineMode("lineStyleFromBlankLine_mode", function() {
   2219    return {token: function(stream) { stream.skipToEnd(); return "comment"; },
   2220            blankLine: function() { return "line-blank"; }};
   2221  });
   2222  cm.setOption("mode", "lineStyleFromBlankLine_mode");
   2223  var blankElts = byClassName(cm.getWrapperElement(), "blank");
   2224  eq(blankElts.length, 1);
   2225  eq(blankElts[0].nodeName, "PRE");
   2226  cm.replaceRange("x", Pos(1, 0));
   2227  blankElts = byClassName(cm.getWrapperElement(), "blank");
   2228  eq(blankElts.length, 0);
   2229 }, {value: "foo\n\nbar"});
   2230 
   2231 CodeMirror.registerHelper("xxx", "a", "A");
   2232 CodeMirror.registerHelper("xxx", "b", "B");
   2233 CodeMirror.defineMode("yyy", function() {
   2234  return {
   2235    token: function(stream) { stream.skipToEnd(); },
   2236    xxx: ["a", "b", "q"]
   2237  };
   2238 });
   2239 CodeMirror.registerGlobalHelper("xxx", "c", function(m) { return m.enableC; }, "C");
   2240 
   2241 testCM("helpers", function(cm) {
   2242  cm.setOption("mode", "yyy");
   2243  eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "A/B");
   2244  cm.setOption("mode", {name: "yyy", modeProps: {xxx: "b", enableC: true}});
   2245  eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "B/C");
   2246  cm.setOption("mode", "javascript");
   2247  eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "");
   2248 });
   2249 
   2250 testCM("selectionHistory", function(cm) {
   2251  for (var i = 0; i < 3; i++) {
   2252    cm.setExtending(true);
   2253    cm.execCommand("goCharRight");
   2254    cm.setExtending(false);
   2255    cm.execCommand("goCharRight");
   2256    cm.execCommand("goCharRight");
   2257  }
   2258  cm.execCommand("undoSelection");
   2259  eq(cm.getSelection(), "c");
   2260  cm.execCommand("undoSelection");
   2261  eq(cm.getSelection(), "");
   2262  eqCursorPos(cm.getCursor(), Pos(0, 4, "before"));
   2263  cm.execCommand("undoSelection");
   2264  eq(cm.getSelection(), "b");
   2265  cm.execCommand("redoSelection");
   2266  eq(cm.getSelection(), "");
   2267  eqCursorPos(cm.getCursor(), Pos(0, 4, "before"));
   2268  cm.execCommand("redoSelection");
   2269  eq(cm.getSelection(), "c");
   2270  cm.execCommand("redoSelection");
   2271  eq(cm.getSelection(), "");
   2272  eqCursorPos(cm.getCursor(), Pos(0, 6, "before"));
   2273 }, {value: "a b c d"});
   2274 
   2275 testCM("selectionChangeReducesRedo", function(cm) {
   2276  cm.replaceSelection("X");
   2277  cm.execCommand("goCharRight");
   2278  cm.undoSelection();
   2279  cm.execCommand("selectAll");
   2280  cm.undoSelection();
   2281  eq(cm.getValue(), "Xabc");
   2282  eqCursorPos(cm.getCursor(), Pos(0, 1));
   2283  cm.undoSelection();
   2284  eq(cm.getValue(), "abc");
   2285 }, {value: "abc"});
   2286 
   2287 testCM("selectionHistoryNonOverlapping", function(cm) {
   2288  cm.setSelection(Pos(0, 0), Pos(0, 1));
   2289  cm.setSelection(Pos(0, 2), Pos(0, 3));
   2290  cm.execCommand("undoSelection");
   2291  eqCursorPos(cm.getCursor("anchor"), Pos(0, 0));
   2292  eqCursorPos(cm.getCursor("head"), Pos(0, 1));
   2293 }, {value: "1234"});
   2294 
   2295 testCM("cursorMotionSplitsHistory", function(cm) {
   2296  cm.replaceSelection("a");
   2297  cm.execCommand("goCharRight");
   2298  cm.replaceSelection("b");
   2299  cm.replaceSelection("c");
   2300  cm.undo();
   2301  eq(cm.getValue(), "a1234");
   2302  eqCursorPos(cm.getCursor(), Pos(0, 2, "before"));
   2303  cm.undo();
   2304  eq(cm.getValue(), "1234");
   2305  eqCursorPos(cm.getCursor(), Pos(0, 0));
   2306 }, {value: "1234"});
   2307 
   2308 testCM("selChangeInOperationDoesNotSplit", function(cm) {
   2309  for (var i = 0; i < 4; i++) {
   2310    cm.operation(function() {
   2311      cm.replaceSelection("x");
   2312      cm.setCursor(Pos(0, cm.getCursor().ch - 1));
   2313    });
   2314  }
   2315  eqCursorPos(cm.getCursor(), Pos(0, 0));
   2316  eq(cm.getValue(), "xxxxa");
   2317  cm.undo();
   2318  eq(cm.getValue(), "a");
   2319 }, {value: "a"});
   2320 
   2321 testCM("alwaysMergeSelEventWithChangeOrigin", function(cm) {
   2322  cm.replaceSelection("U", null, "foo");
   2323  cm.setSelection(Pos(0, 0), Pos(0, 1), {origin: "foo"});
   2324  cm.undoSelection();
   2325  eq(cm.getValue(), "a");
   2326  cm.replaceSelection("V", null, "foo");
   2327  cm.setSelection(Pos(0, 0), Pos(0, 1), {origin: "bar"});
   2328  cm.undoSelection();
   2329  eq(cm.getValue(), "Va");
   2330 }, {value: "a"});
   2331 
   2332 testCM("getTokenAt", function(cm) {
   2333  var tokPlus = cm.getTokenAt(Pos(0, 2));
   2334  eq(tokPlus.type, "operator");
   2335  eq(tokPlus.string, "+");
   2336  var toks = cm.getLineTokens(0);
   2337  eq(toks.length, 3);
   2338  forEach([["number", "1"], ["operator", "+"], ["number", "2"]], function(expect, i) {
   2339    eq(toks[i].type, expect[0]);
   2340    eq(toks[i].string, expect[1]);
   2341  });
   2342 }, {value: "1+2", mode: "javascript"});
   2343 
   2344 testCM("getTokenTypeAt", function(cm) {
   2345  eq(cm.getTokenTypeAt(Pos(0, 0)), "number");
   2346  eq(cm.getTokenTypeAt(Pos(0, 6)), "string");
   2347  cm.addOverlay({
   2348    token: function(stream) {
   2349      if (stream.match("foo")) return "foo";
   2350      else stream.next();
   2351    }
   2352  });
   2353  eq(byClassName(cm.getWrapperElement(), "cm-foo").length, 1);
   2354  eq(cm.getTokenTypeAt(Pos(0, 6)), "string");
   2355 }, {value: "1 + 'foo'", mode: "javascript"});
   2356 
   2357 testCM("addOverlay", function(cm) {
   2358  cm.addOverlay({
   2359    token: function(stream) {
   2360      var base = stream.baseToken()
   2361      if (!/comment/.test(base.type) && stream.match(/\d+/)) return "x"
   2362      stream.next()
   2363    }
   2364  })
   2365  var x = byClassName(cm.getWrapperElement(), "cm-x")
   2366  is(x.length, 1)
   2367  is(x[0].textContent, "233")
   2368  cm.replaceRange("", Pos(0, 4), Pos(0, 6))
   2369  is(byClassName(cm.getWrapperElement(), "cm-x").length, 2)
   2370 }, {value: "foo /* 100 */\nbar + 233;\nbaz", mode: "javascript"})
   2371 
   2372 testCM("resizeLineWidget", function(cm) {
   2373  addDoc(cm, 200, 3);
   2374  var widget = document.createElement("pre");
   2375  widget.innerHTML = "imwidget";
   2376  widget.style.background = "yellow";
   2377  cm.addLineWidget(1, widget, {noHScroll: true});
   2378  cm.setSize(40);
   2379  is(widget.parentNode.offsetWidth < 42);
   2380 });
   2381 
   2382 testCM("combinedOperations", function(cm) {
   2383  var place = document.getElementById("testground");
   2384  var other = CodeMirror(place, {value: "123"});
   2385  try {
   2386    cm.operation(function() {
   2387      cm.addLineClass(0, "wrap", "foo");
   2388      other.addLineClass(0, "wrap", "foo");
   2389    });
   2390    eq(byClassName(cm.getWrapperElement(), "foo").length, 1);
   2391    eq(byClassName(other.getWrapperElement(), "foo").length, 1);
   2392    cm.operation(function() {
   2393      cm.removeLineClass(0, "wrap", "foo");
   2394      other.removeLineClass(0, "wrap", "foo");
   2395    });
   2396    eq(byClassName(cm.getWrapperElement(), "foo").length, 0);
   2397    eq(byClassName(other.getWrapperElement(), "foo").length, 0);
   2398  } finally {
   2399    place.removeChild(other.getWrapperElement());
   2400  }
   2401 }, {value: "abc"});
   2402 
   2403 testCM("eventOrder", function(cm) {
   2404  var seen = [];
   2405  cm.on("change", function() {
   2406    if (!seen.length) cm.replaceSelection(".");
   2407    seen.push("change");
   2408  });
   2409  cm.on("cursorActivity", function() {
   2410    cm.replaceSelection("!");
   2411    seen.push("activity");
   2412  });
   2413  cm.replaceSelection("/");
   2414  eq(seen.join(","), "change,change,activity,change");
   2415 });
   2416 
   2417 testCM("splitSpaces_nonspecial", function(cm) {
   2418  eq(byClassName(cm.getWrapperElement(), "cm-invalidchar").length, 0);
   2419 }, {
   2420  specialChars: /[\u00a0]/,
   2421  value: "spaces ->            <- between"
   2422 });
   2423 
   2424 test("core_rmClass", function() {
   2425  var node = document.createElement("div");
   2426  node.className = "foo-bar baz-quux yadda";
   2427  CodeMirror.rmClass(node, "quux");
   2428  eq(node.className, "foo-bar baz-quux yadda");
   2429  CodeMirror.rmClass(node, "baz-quux");
   2430  eq(node.className, "foo-bar yadda");
   2431  CodeMirror.rmClass(node, "yadda");
   2432  eq(node.className, "foo-bar");
   2433  CodeMirror.rmClass(node, "foo-bar");
   2434  eq(node.className, "");
   2435  node.className = " foo ";
   2436  CodeMirror.rmClass(node, "foo");
   2437  eq(node.className, "");
   2438 });
   2439 
   2440 test("core_addClass", function() {
   2441  var node = document.createElement("div");
   2442  CodeMirror.addClass(node, "a");
   2443  eq(node.className, "a");
   2444  CodeMirror.addClass(node, "a");
   2445  eq(node.className, "a");
   2446  CodeMirror.addClass(node, "b");
   2447  eq(node.className, "a b");
   2448  CodeMirror.addClass(node, "a");
   2449  CodeMirror.addClass(node, "b");
   2450  eq(node.className, "a b");
   2451 });
   2452 
   2453 testCM("lineSeparator", function(cm) {
   2454  eq(cm.lineCount(), 3);
   2455  eq(cm.getLine(1), "bar\r");
   2456  eq(cm.getLine(2), "baz\rquux");
   2457  cm.setOption("lineSeparator", "\r");
   2458  eq(cm.lineCount(), 5);
   2459  eq(cm.getLine(4), "quux");
   2460  eq(cm.getValue(), "foo\rbar\r\rbaz\rquux");
   2461  eq(cm.getValue("\n"), "foo\nbar\n\nbaz\nquux");
   2462  cm.setOption("lineSeparator", null);
   2463  cm.setValue("foo\nbar\r\nbaz\rquux");
   2464  eq(cm.lineCount(), 4);
   2465 }, {value: "foo\nbar\r\nbaz\rquux",
   2466    lineSeparator: "\n"});
   2467 
   2468 var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/
   2469 var getChar = function (noExtending) { var res; do {res = String.fromCharCode(Math.floor(Math.random()*0x8ac)); } while ([0x90].indexOf(res.charCodeAt(0)) != -1 || (noExtending && extendingChars.test(res))); return res }
   2470 var getString = function (n) { var res = getChar(true); while (--n > 0) res += getChar(); return res }
   2471 
   2472 function makeItWrapAfter(cm, pos) {
   2473  var firstLineTop = cm.cursorCoords(Pos(0, 0)).top;
   2474  for(var w = 0, posTop; posTop != firstLineTop; ++w) {
   2475    cm.setSize(w);
   2476    posTop = cm.charCoords(pos).top;
   2477  }
   2478 }
   2479 
   2480 function countIf(arr, f) {
   2481  var result = 0
   2482  for (var i = 0; i < arr.length; i++) if (f[arr[i]]) result++
   2483  return result
   2484 }
   2485 
   2486 function testMoveBidi(str) {
   2487  testCM("move_bidi_" + str, function(cm) {
   2488    if (cm.getOption("inputStyle") != "textarea" || !cm.getOption("rtlMoveVisually")) return;
   2489    cm.getScrollerElement().style.fontFamily = "monospace";
   2490    makeItWrapAfter(cm, Pos(0, 5));
   2491 
   2492    var steps = str.length - countIf(str.split(""), function(ch) { return extendingChars.test(ch) });
   2493    var lineBreaks = {}
   2494    lineBreaks[6 - countIf(str.substr(0, 5).split(""), function(ch) { return extendingChars.test(ch) })] = 'w';
   2495    if (str.indexOf("\n") != -1) {
   2496      lineBreaks[steps - 2] = 'n';
   2497    }
   2498 
   2499    // Make sure we are at the visual beginning of the first line
   2500    cm.execCommand("goLineStart");
   2501 
   2502    var prevCoords = cm.cursorCoords(), coords;
   2503    for(var i = 0; i < steps; ++i) {
   2504      cm.execCommand("goCharRight");
   2505      coords = cm.cursorCoords();
   2506      if ((i >= 10 && i <= 12) && !lineBreaks[i] && coords.left < prevCoords.left && coords.top > prevCoords.top) {
   2507        // The first line wraps twice
   2508        lineBreaks[i] = 'w';
   2509      }
   2510      if (!lineBreaks[i]) {
   2511        is(coords.left > prevCoords.left, "In step " + i + ", cursor didn't move right");
   2512        eq(coords.top, prevCoords.top, "In step " + i + ", cursor moved out of line");
   2513      } else {
   2514        is(coords.left < prevCoords.left, i);
   2515        is(coords.top > prevCoords.top, i);
   2516      }
   2517      prevCoords = coords;
   2518    }
   2519 
   2520    cm.execCommand("goCharRight");
   2521    coords = cm.cursorCoords();
   2522    eq(coords.left, prevCoords.left, "Moving " + steps + " steps right didn't reach the end");
   2523    eq(coords.top, prevCoords.top, "Moving " + steps + " steps right didn't reach the end");
   2524 
   2525    for(i = steps - 1; i >= 0; --i) {
   2526      cm.execCommand("goCharLeft");
   2527      coords = cm.cursorCoords();
   2528      if (!(lineBreaks[i] == 'n' || lineBreaks[i + 1] == 'w')) {
   2529        is(coords.left < prevCoords.left, "In step " + i + ", cursor didn't move left");
   2530        eq(coords.top, prevCoords.top, "In step " + i + ", cursor is not at the same line anymore");
   2531      } else {
   2532        is(coords.left > prevCoords.left, i);
   2533        is(coords.top < prevCoords.top, i);
   2534      }
   2535      prevCoords = coords;
   2536    }
   2537 
   2538    cm.execCommand("goCharLeft");
   2539    coords = cm.cursorCoords();
   2540    eq(coords.left, prevCoords.left, "Moving " + steps + " steps left didn't reach the beginning");
   2541    eq(coords.top, prevCoords.top, "Moving " + steps + " steps left didn't reach the beginning");
   2542  }, {value: str, lineWrapping: true})
   2543 };
   2544 
   2545 function testMoveEndBidi(str) {
   2546  testCM("move_end_bidi_" + str, function(cm) {
   2547    cm.getScrollerElement().style.fontFamily = "monospace";
   2548    makeItWrapAfter(cm, Pos(0, 5));
   2549 
   2550    cm.execCommand("goLineStart");
   2551    var pos = cm.doc.getCursor();
   2552    cm.execCommand("goCharLeft");
   2553    eqCursorPos(pos, cm.doc.getCursor());
   2554 
   2555    cm.execCommand("goLineEnd");
   2556    pos = cm.doc.getCursor();
   2557    cm.execCommand("goColumnRight");
   2558    eqCursorPos(pos, cm.doc.getCursor());
   2559  }, {value: str, lineWrapping: true})
   2560 };
   2561 
   2562 var bidiTests = [];
   2563 
   2564 // We don't correctly implement L1 UBA
   2565 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1331501
   2566 // and https://bugs.chromium.org/p/chromium/issues/detail?id=673405
   2567 /*
   2568 bidiTests.push("Say ا ب جabj\nS");
   2569 bidiTests.push("Sayyy ا ا ب ج");
   2570 */
   2571 
   2572 if (!phantom) {
   2573  bidiTests.push("Όȝǝڪȉۥ״ۺ׆ɀҩۏ\nҳ");
   2574  bidiTests.push("ŌӰтقȤ؁ƥ؅٣ĎȺ١\nϚ");
   2575  bidiTests.push("ٻоҤѕѽΩ־؉ïίքdz\nٵ");
   2576  bidiTests.push("؅؁ĆՕƿɁǞϮؠȩóć\nď");
   2577  bidiTests.push("RŨďңŪzϢŎƏԖڇڦ\nӈ");
   2578  bidiTests.push("ό׊۷٢ԜһОצЉيčǟ\nѩ");
   2579  bidiTests.push("ۑÚҳҕڬġڹհяųKV\nr");
   2580  bidiTests.push("źڻғúہ4ם1Ƞc1a\nԁ");
   2581  bidiTests.push("ҒȨҟփƞ٦ԓȦڰғâƥ\nڤ");
   2582  bidiTests.push("ϖسՉȏŧΔԛdžĎӟیڡ\nέ");
   2583  bidiTests.push("۹ؼL۵ĺȧКԙػא7״\nم");
   2584  bidiTests.push("ن (ي)\u2009أقواس"); // thin space to throw off Firefox 51's broken white-space compressing behavior
   2585 }
   2586 
   2587 bidiTests.push("քմѧǮßپüŢҍҞўڳ\nӧ");
   2588 
   2589 //bidiTests.push("Count ١ ٢ ٣ ٤");
   2590 //bidiTests.push("ӣאƦϰ؊ȓېÛوը٬ز\nϪ");
   2591 //bidiTests.push("ҾճٳџIՖӻ٥׭֐؜ڏ\nێ");
   2592 //bidiTests.push("ҬÓФ؜ڂį٦Ͽɓڐͳٵ\nՈ");
   2593 //bidiTests.push("aѴNijȻهˇ҃ڱӧǻֵ\na");
   2594 //bidiTests.push(" a٧ا٢ ب جa\nS");
   2595 
   2596 for (var i = 0; i < bidiTests.length; ++i) {
   2597  testMoveBidi(bidiTests[i]);
   2598  testMoveEndBidi(bidiTests[i]);
   2599 }
   2600 
   2601 /*
   2602 for (var i = 0; i < 5; ++i) {
   2603  testMoveBidi(getString(12) + "\n" + getString(1));
   2604 }
   2605 */
   2606 
   2607 function testCoordsWrappedBidi(str) {
   2608  testCM("coords_wrapped_bidi_" + str, function(cm) {
   2609    cm.getScrollerElement().style.fontFamily = "monospace";
   2610    makeItWrapAfter(cm, Pos(0, 5));
   2611 
   2612    // Make sure we are at the visual beginning of the first line
   2613    var pos = Pos(0, 0), lastPos;
   2614    cm.doc.setCursor(pos);
   2615    do {
   2616      lastPos = pos;
   2617      cm.execCommand("goCharLeft");
   2618      pos = cm.doc.getCursor();
   2619    } while (pos != lastPos)
   2620 
   2621    var top = cm.charCoords(Pos(0, 0)).top, lastTop;
   2622    for (var i = 1; i < str.length; ++i) {
   2623      lastTop = top;
   2624      top = cm.charCoords(Pos(0, i)).top;
   2625      is(top >= lastTop);
   2626    }
   2627  }, {value: str, lineWrapping: true})
   2628 };
   2629 
   2630 testCoordsWrappedBidi("Count ١ ٢ ٣ ٤");
   2631 /*
   2632 for (var i = 0; i < 5; ++i) {
   2633  testCoordsWrappedBidi(getString(50));
   2634 }
   2635 */
   2636 
   2637 testCM("rtl_wrapped_selection", function(cm) {
   2638  cm.setSelection(Pos(0, 10), Pos(0, 190))
   2639  is(byClassName(cm.getWrapperElement(), "CodeMirror-selected").length >= 3)
   2640 }, {value: new Array(10).join(" فتي تم تضمينها فتي تم"), lineWrapping: true})
   2641 
   2642 testCM("bidi_wrapped_selection", function(cm) {
   2643  if (phantom) return
   2644  cm.setSize(cm.charCoords(Pos(0, 10), "editor").left)
   2645  cm.setSelection(Pos(0, 37), Pos(0, 80))
   2646  var blocks = byClassName(cm.getWrapperElement(), "CodeMirror-selected")
   2647  is(blocks.length >= 2)
   2648  is(blocks.length <= 3)
   2649  var boxTop = blocks[0].getBoundingClientRect(), boxBot = blocks[blocks.length - 1].getBoundingClientRect()
   2650  is(boxTop.left > cm.charCoords(Pos(0, 1)).right)
   2651  is(boxBot.right < cm.charCoords(Pos(0, cm.getLine(0).length - 2)).left)
   2652 }, {value: "<p>مفتي11 تم تضمينهفتي تم تضمينها فتي تفتي تم تضمينها فتي تفتي تم تضمينها فتي تفتي تم تضمينها فتي تا فت10ي ت</p>", lineWrapping: true})
   2653 
   2654 testCM("delete_wrapped", function(cm) {
   2655  makeItWrapAfter(cm, Pos(0, 2));
   2656  cm.doc.setCursor(Pos(0, 3, "after"));
   2657  cm.deleteH(-1, "char");
   2658  eq(cm.getLine(0), "1245");
   2659 }, {value: "12345", lineWrapping: true})
   2660 
   2661 testCM("issue_4878", function(cm) {
   2662  if (phantom) return
   2663  cm.setCursor(Pos(1, 12, "after"));
   2664  cm.moveH(-1, "char");
   2665  eqCursorPos(cm.getCursor(), Pos(0, 113, "before"));
   2666 }, {value: "  في تطبيق السمات مرة واحدة https://github.com/codemirror/CodeMirror/issues/4878#issuecomment-330550964على سبيل المثال <code>\"foo bar\"</code>\n" +
   2667 "  سيتم تعيين", direction: "rtl", lineWrapping: true});
   2668 
   2669 CodeMirror.defineMode("lookahead_mode", function() {
   2670  // Colors text as atom if the line two lines down has an x in it
   2671  return {
   2672    token: function(stream) {
   2673      stream.skipToEnd()
   2674      return /x/.test(stream.lookAhead(2)) ? "atom" : null
   2675    }
   2676  }
   2677 })
   2678 
   2679 testCM("mode_lookahead", function(cm) {
   2680  eq(cm.getTokenAt(Pos(0, 1)).type, "atom")
   2681  eq(cm.getTokenAt(Pos(1, 1)).type, "atom")
   2682  eq(cm.getTokenAt(Pos(2, 1)).type, null)
   2683  cm.replaceRange("\n", Pos(2, 0))
   2684  eq(cm.getTokenAt(Pos(0, 1)).type, null)
   2685  eq(cm.getTokenAt(Pos(1, 1)).type, "atom")
   2686 }, {value: "foo\na\nx\nx\n", mode: "lookahead_mode"})