Mercurial > kallithea
changeset 1606:a30689fc4f61 beta
bumped codemirror to latest version
author | Marcin Kuzminski <marcin@python-works.com> |
---|---|
date | Thu, 27 Oct 2011 19:05:37 +0200 |
parents | df59c0503636 |
children | e886f91fcb71 |
files | rhodecode/public/js/codemirror.js |
diffstat | 1 files changed, 755 insertions(+), 338 deletions(-) [+] |
line wrap: on
line diff
--- a/rhodecode/public/js/codemirror.js Thu Oct 27 03:26:02 2011 +0200 +++ b/rhodecode/public/js/codemirror.js Thu Oct 27 19:05:37 2011 +0200 @@ -13,32 +13,45 @@ if (defaults.hasOwnProperty(opt)) options[opt] = (givenOptions && givenOptions.hasOwnProperty(opt) ? givenOptions : defaults)[opt]; - // The element in which the editor lives. Takes care of scrolling - // (if enabled). - var wrapper = document.createElement("div"); + var targetDocument = options["document"]; + // The element in which the editor lives. + var wrapper = targetDocument.createElement("div"); wrapper.className = "CodeMirror"; // This mess creates the base DOM structure for the editor. wrapper.innerHTML = - '<div style="position: relative">' + // Set to the height of the text, causes scrolling - '<pre style="position: relative; height: 0; visibility: hidden; overflow: hidden;">' + // To measure line/char size - '<span>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span></pre>' + - '<div style="position: relative">' + // Moved around its parent to cover visible view - '<div class="CodeMirror-gutter"><div class="CodeMirror-gutter-text"></div></div>' + - '<div style="overflow: hidden; position: absolute; width: 0; left: 0">' + // Wraps and hides input textarea - '<textarea style="height: 1px; position: absolute; width: 1px;" wrap="off"></textarea></div>' + - // Provides positioning relative to (visible) text origin - '<div class="CodeMirror-lines"><div style="position: relative">' + - '<pre class="CodeMirror-cursor"> </pre>' + // Absolutely positioned blinky cursor - '<div></div></div></div></div></div>'; // This DIV contains the actual code + '<div style="overflow: hidden; position: relative; width: 1px; height: 0px;">' + // Wraps and hides input textarea + '<textarea style="position: absolute; width: 10000px;" wrap="off" ' + + 'autocorrect="off" autocapitalize="off"></textarea></div>' + + '<div class="CodeMirror-scroll cm-s-' + options.theme + '">' + + '<div style="position: relative">' + // Set to the height of the text, causes scrolling + '<div style="position: absolute; height: 0; width: 0; overflow: hidden;"></div>' + + '<div style="position: relative">' + // Moved around its parent to cover visible view + '<div class="CodeMirror-gutter"><div class="CodeMirror-gutter-text"></div></div>' + + // Provides positioning relative to (visible) text origin + '<div class="CodeMirror-lines"><div style="position: relative" draggable="true">' + + '<pre class="CodeMirror-cursor"> </pre>' + // Absolutely positioned blinky cursor + '<div></div>' + // This DIV contains the actual code + '</div></div></div></div></div>'; if (place.appendChild) place.appendChild(wrapper); else place(wrapper); // I've never seen more elegant code in my life. - var code = wrapper.firstChild, measure = code.firstChild, mover = measure.nextSibling, + var inputDiv = wrapper.firstChild, input = inputDiv.firstChild, + scroller = wrapper.lastChild, code = scroller.firstChild, + measure = code.firstChild, mover = measure.nextSibling, gutter = mover.firstChild, gutterText = gutter.firstChild, - inputDiv = gutter.nextSibling, input = inputDiv.firstChild, - lineSpace = inputDiv.nextSibling.firstChild, cursor = lineSpace.firstChild, lineDiv = cursor.nextSibling; + lineSpace = gutter.nextSibling.firstChild, + cursor = lineSpace.firstChild, lineDiv = cursor.nextSibling; if (options.tabindex != null) input.tabindex = options.tabindex; if (!options.gutter && !options.lineNumbers) gutter.style.display = "none"; + // Check for problem with IE innerHTML not working when we have a + // P (or similar) parent node. + try { stringWidth("x"); } + catch (e) { + if (e.message.match(/unknown runtime/i)) + e = new Error("A CodeMirror inside a P-style element does not work in Internet Explorer. (innerHTML bug)"); + throw e; + } + // Delayed object wrap timeouts, making sure only one is active. blinker holds an interval. var poll = new Delayed(), highlight = new Delayed(), blinker; @@ -46,7 +59,7 @@ // (see Line constructor), work an array of lines that should be // parsed, and history the undo history (instance of History // constructor). - var mode, lines = [new Line("")], work, history = new History(), focused; + var mode, lines = [new Line("")], work, focused; loadMode(); // The selection. These are always maintained to point at valid // positions. Inverted is used to remember that the user is @@ -56,10 +69,10 @@ // whether the user is holding shift. reducedSelection is a hack // to get around the fact that we can't create inverted // selections. See below. - var shiftSelecting, reducedSelection; + var shiftSelecting, reducedSelection, lastClick, lastDoubleClick, draggingText; // Variables used by startOperation/endOperation to track what // happened during the operation. - var updateInput, changes, textChanged, selectionChanged, leaveInputAlone; + var updateInput, changes, textChanged, selectionChanged, leaveInputAlone, gutterDirty; // Current visible range (may be bigger than the view window). var showingFrom = 0, showingTo = 0, lastHeight = 0, curKeyId = null; // editing will hold an object describing the things we put in the @@ -67,35 +80,46 @@ // bracketHighlighted is used to remember that a backet has been // marked. var editing, bracketHighlighted; + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + var maxLine = "", maxWidth; - // Initialize the content. Somewhat hacky (delayed prepareInput) - // to work around browser issues. + // Initialize the content. operation(function(){setValue(options.value || ""); updateInput = false;})(); - setTimeout(prepareInput, 20); + var history = new History(); // Register our event handlers. - connect(wrapper, "mousedown", operation(onMouseDown)); + connect(scroller, "mousedown", operation(onMouseDown)); + connect(scroller, "dblclick", operation(onDoubleClick)); + connect(lineSpace, "dragstart", onDragStart); // Gecko browsers fire contextmenu *after* opening the menu, at // which point we can't mess with it anymore. Context menu is // handled in onMouseDown for Gecko. - if (!gecko) connect(wrapper, "contextmenu", operation(onContextMenu)); - connect(code, "dblclick", operation(onDblClick)); - connect(wrapper, "scroll", function() {updateDisplay([]); if (options.onScroll) options.onScroll(instance);}); + if (!gecko) connect(scroller, "contextmenu", onContextMenu); + connect(scroller, "scroll", function() { + updateDisplay([]); + if (options.fixedGutter) gutter.style.left = scroller.scrollLeft + "px"; + if (options.onScroll) options.onScroll(instance); + }); connect(window, "resize", function() {updateDisplay(true);}); connect(input, "keyup", operation(onKeyUp)); + connect(input, "input", function() {fastPoll(curKeyId);}); connect(input, "keydown", operation(onKeyDown)); connect(input, "keypress", operation(onKeyPress)); connect(input, "focus", onFocus); connect(input, "blur", onBlur); - connect(wrapper, "dragenter", function(e){e.stop();}); - connect(wrapper, "dragover", function(e){e.stop();}); - connect(wrapper, "drop", operation(onDrop)); - connect(wrapper, "paste", function(){input.focus(); fastPoll();}); + connect(scroller, "dragenter", e_stop); + connect(scroller, "dragover", e_stop); + connect(scroller, "drop", operation(onDrop)); + connect(scroller, "paste", function(){focusInput(); fastPoll();}); connect(input, "paste", function(){fastPoll();}); connect(input, "cut", function(){fastPoll();}); - if (document.activeElement == input) onFocus(); + // IE throws unspecified error in certain cases, when + // trying to access activeElement before onload + var hasFocus; try { hasFocus = (targetDocument.activeElement == input); } catch(e) { } + if (hasFocus) setTimeout(onFocus, 20); else onBlur(); function isLine(l) {return l >= 0 && l < lines.length;} @@ -104,27 +128,37 @@ // range checking and/or clipping. operation is used to wrap the // call so that changes it makes are tracked, and the display is // updated afterwards. - var instance = { + var instance = wrapper.CodeMirror = { getValue: getValue, setValue: operation(setValue), getSelection: getSelection, replaceSelection: operation(replaceSelection), - focus: function(){input.focus(); onFocus(); fastPoll();}, + focus: function(){focusInput(); onFocus(); fastPoll();}, setOption: function(option, value) { options[option] = value; - if (option == "lineNumbers" || option == "gutter") gutterChanged(); + if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber") + operation(gutterChanged)(); else if (option == "mode" || option == "indentUnit") loadMode(); + else if (option == "readOnly" && value == "nocursor") input.blur(); + else if (option == "theme") scroller.className = scroller.className.replace(/cm-s-\w+/, "cm-s-" + value); }, getOption: function(option) {return options[option];}, undo: operation(undo), redo: operation(redo), - indentLine: operation(function(n) {if (isLine(n)) indentLine(n, "smart");}), + indentLine: operation(function(n, dir) { + if (isLine(n)) indentLine(n, dir == null ? "smart" : dir ? "add" : "subtract"); + }), historySize: function() {return {undo: history.done.length, redo: history.undone.length};}, + clearHistory: function() {history = new History();}, matchBrackets: operation(function(){matchBrackets(true);}), getTokenAt: function(pos) { pos = clipPos(pos); return lines[pos.line].getTokenAt(mode, getStateBefore(pos.line), pos.ch); }, + getStateAfter: function(line) { + line = clipLine(line == null ? lines.length - 1: line); + return getStateBefore(line + 1); + }, cursorCoords: function(start){ if (start == null) start = sel.inverted; return pageCoords(start ? sel.from : sel.to); @@ -132,22 +166,41 @@ charCoords: function(pos){return pageCoords(clipPos(pos));}, coordsChar: function(coords) { var off = eltOffset(lineSpace); - var line = Math.min(showingTo - 1, showingFrom + Math.floor(coords.y / lineHeight())); - return clipPos({line: line, ch: charFromX(clipLine(line), coords.x)}); + var line = clipLine(Math.min(lines.length - 1, showingFrom + Math.floor((coords.y - off.top) / lineHeight()))); + return clipPos({line: line, ch: charFromX(clipLine(line), coords.x - off.left)}); }, getSearchCursor: function(query, pos, caseFold) {return new SearchCursor(query, pos, caseFold);}, - markText: operation(function(a, b, c){return operation(markText(a, b, c));}), - setMarker: addGutterMarker, - clearMarker: removeGutterMarker, + markText: operation(markText), + setMarker: operation(addGutterMarker), + clearMarker: operation(removeGutterMarker), setLineClass: operation(setLineClass), lineInfo: lineInfo, - addWidget: function(pos, node, scroll) { - var pos = localCoords(clipPos(pos), true); - node.style.top = (showingFrom * lineHeight() + pos.yBot + paddingTop()) + "px"; - node.style.left = (pos.x + paddingLeft()) + "px"; + addWidget: function(pos, node, scroll, vert, horiz) { + pos = localCoords(clipPos(pos)); + var top = pos.yBot, left = pos.x; + node.style.position = "absolute"; code.appendChild(node); + if (vert == "over") top = pos.y; + else if (vert == "near") { + var vspace = Math.max(scroller.offsetHeight, lines.length * lineHeight()), + hspace = Math.max(code.clientWidth, lineSpace.clientWidth) - paddingLeft(); + if (pos.yBot + node.offsetHeight > vspace && pos.y > node.offsetHeight) + top = pos.y - node.offsetHeight; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = (top + paddingTop()) + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = code.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (code.clientWidth - node.offsetWidth) / 2; + node.style.left = (left + paddingLeft()) + "px"; + } if (scroll) - scrollIntoView(pos.x, pos.yBot, pos.x + node.offsetWidth, pos.yBot + node.offsetHeight); + scrollIntoView(left, top, left + node.offsetWidth, top + node.offsetHeight); }, lineCount: function() {return lines.length;}, @@ -171,18 +224,30 @@ replaceRange: operation(replaceRange), getRange: function(from, to) {return getRange(clipPos(from), clipPos(to));}, + coordsFromIndex: function(index) { + var total = lines.length, pos = 0, line, ch, len; + + for (line = 0; line < total; line++) { + len = lines[line].text.length + 1; + if (pos + len > index) { ch = index - pos; break; } + pos += len; + } + return clipPos({line: line, ch: ch}); + }, + operation: function(f){return operation(f)();}, refresh: function(){updateDisplay(true);}, getInputField: function(){return input;}, - getWrapperElement: function(){return wrapper;} + getWrapperElement: function(){return wrapper;}, + getScrollerElement: function(){return scroller;}, + getGutterElement: function(){return gutter;} }; function setValue(code) { - history = null; var top = {line: 0, ch: 0}; updateLines(top, {line: lines.length - 1, ch: lines[lines.length-1].text.length}, splitLines(code), top, top); - history = new History(); + updateInput = true; } function getValue(code) { var text = []; @@ -192,38 +257,70 @@ } function onMouseDown(e) { + // Check whether this is a click in a widget + for (var n = e_target(e); n != wrapper; n = n.parentNode) + if (n.parentNode == code && n != mover) return; + // First, see if this is a click in the gutter - for (var n = e.target(); n != wrapper; n = n.parentNode) + for (var n = e_target(e); n != wrapper; n = n.parentNode) if (n.parentNode == gutterText) { if (options.onGutterClick) - options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom); - return e.stop(); + options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom, e); + return e_preventDefault(e); } - if (gecko && e.button() == 3) onContextMenu(e); - if (e.button() != 1) return; + var start = posFromMouse(e); + + switch (e_button(e)) { + case 3: + if (gecko && !mac) onContextMenu(e); + return; + case 2: + if (start) setCursor(start.line, start.ch, true); + return; + } // For button 1, if it was clicked inside the editor // (posFromMouse returning non-null), we have to adjust the // selection. - var start = posFromMouse(e), last = start, going; - if (!start) {if (e.target() == wrapper) e.stop(); return;} - setCursor(start.line, start.ch, false); + if (!start) {if (e_target(e) == scroller) e_preventDefault(e); return;} if (!focused) onFocus(); - e.stop(); - // And then we have to see if it's a drag event, in which case - // the dragged-over text must be selected. - function end() { - input.focus(); - updateInput = true; - move(); up(); + + var now = +new Date; + if (lastDoubleClick > now - 400) { + e_preventDefault(e); + return selectLine(start.line); + } else if (lastClick > now - 400) { + lastDoubleClick = now; + e_preventDefault(e); + return selectWordAt(start); + } else { lastClick = now; } + + var last = start, going; + if (dragAndDrop && !posEq(sel.from, sel.to) && + !posLess(start, sel.from) && !posLess(sel.to, start)) { + // Let the drag handler handle this. + var up = connect(targetDocument, "mouseup", operation(function(e2) { + draggingText = false; + up(); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + setCursor(start.line, start.ch, true); + focusInput(); + } + }), true); + draggingText = true; + return; } + e_preventDefault(e); + setCursor(start.line, start.ch, true); + function extend(e) { var cur = posFromMouse(e, true); if (cur && !posEq(cur, last)) { if (!focused) onFocus(); last = cur; - setSelection(start, cur); + setSelectionUser(start, cur); updateInput = false; var visible = visibleLines(); if (cur.line >= visible.to || cur.line < visible.from) @@ -231,68 +328,95 @@ } } - var move = connect(document, "mousemove", operation(function(e) { + var move = connect(targetDocument, "mousemove", operation(function(e) { clearTimeout(going); - e.stop(); + e_preventDefault(e); extend(e); }), true); - var up = connect(document, "mouseup", operation(function(e) { + var up = connect(targetDocument, "mouseup", operation(function(e) { clearTimeout(going); var cur = posFromMouse(e); - if (cur) setSelection(start, cur); - e.stop(); - end(); + if (cur) setSelectionUser(start, cur); + e_preventDefault(e); + focusInput(); + updateInput = true; + move(); up(); }), true); } - function onDblClick(e) { - var pos = posFromMouse(e); - if (!pos) return; - selectWordAt(pos); - e.stop(); + function onDoubleClick(e) { + var start = posFromMouse(e); + if (!start) return; + lastDoubleClick = +new Date; + e_preventDefault(e); + selectWordAt(start); } function onDrop(e) { - var pos = posFromMouse(e, true), files = e.e.dataTransfer.files; + e.preventDefault(); + var pos = posFromMouse(e, true), files = e.dataTransfer.files; if (!pos || options.readOnly) return; if (files && files.length && window.FileReader && window.File) { - var n = files.length, text = Array(n), read = 0; - for (var i = 0; i < n; ++i) loadFile(files[i], i); function loadFile(file, i) { var reader = new FileReader; reader.onload = function() { text[i] = reader.result; - if (++read == n) replaceRange(text.join(""), clipPos(pos), clipPos(pos)); + if (++read == n) { + pos = clipPos(pos); + var end = replaceRange(text.join(""), pos, pos); + setSelectionUser(pos, end); + } }; reader.readAsText(file); } + var n = files.length, text = Array(n), read = 0; + for (var i = 0; i < n; ++i) loadFile(files[i], i); } else { try { - var text = e.e.dataTransfer.getData("Text"); - if (text) replaceRange(text, pos, pos); + var text = e.dataTransfer.getData("Text"); + if (text) { + var end = replaceRange(text, pos, pos); + var curFrom = sel.from, curTo = sel.to; + setSelectionUser(pos, end); + if (draggingText) replaceRange("", curFrom, curTo); + focusInput(); + } } catch(e){} } } + function onDragStart(e) { + var txt = getSelection(); + // This will reset escapeElement + htmlEscape(txt); + e.dataTransfer.setDragImage(escapeElement, 0, 0); + e.dataTransfer.setData("Text", txt); + } function onKeyDown(e) { if (!focused) onFocus(); - var code = e.e.keyCode; + var code = e.keyCode; + // IE does strange things with escape. + if (ie && code == 27) { e.returnValue = false; } // Tries to detect ctrl on non-mac, cmd on mac. - var mod = (mac ? e.e.metaKey : e.e.ctrlKey) && !e.e.altKey, anyMod = e.e.ctrlKey || e.e.altKey || e.e.metaKey; - if (code == 16 || e.e.shiftKey) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from); + var mod = (mac ? e.metaKey : e.ctrlKey) && !e.altKey, anyMod = e.ctrlKey || e.altKey || e.metaKey; + if (code == 16 || e.shiftKey) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from); else shiftSelecting = null; // First give onKeyEvent option a chance to handle this. - if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e.e))) return; + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; - if (code == 33 || code == 34) {scrollPage(code == 34); return e.stop();} // page up/down - if (mod && (code == 36 || code == 35)) {scrollEnd(code == 36); return e.stop();} // ctrl-home/end - if (mod && code == 65) {selectAll(); return e.stop();} // ctrl-a + if (code == 33 || code == 34) {scrollPage(code == 34); return e_preventDefault(e);} // page up/down + if (mod && ((code == 36 || code == 35) || // ctrl-home/end + mac && (code == 38 || code == 40))) { // cmd-up/down + scrollEnd(code == 36 || code == 38); return e_preventDefault(e); + } + if (mod && code == 65) {selectAll(); return e_preventDefault(e);} // ctrl-a if (!options.readOnly) { if (!anyMod && code == 13) {return;} // enter - if (!anyMod && code == 9 && handleTab(e.e.shiftKey)) return e.stop(); // tab - if (mod && code == 90) {undo(); return e.stop();} // ctrl-z - if (mod && ((e.e.shiftKey && code == 90) || code == 89)) {redo(); return e.stop();} // ctrl-shift-z, ctrl-y + if (!anyMod && code == 9 && handleTab(e.shiftKey)) return e_preventDefault(e); // tab + if (mod && code == 90) {undo(); return e_preventDefault(e);} // ctrl-z + if (mod && ((e.shiftKey && code == 90) || code == 89)) {redo(); return e_preventDefault(e);} // ctrl-shift-z, ctrl-y } + if (code == 36) { if (options.smartHome) { smartHome(); return e_preventDefault(e); } } // Key id to use in the movementKeys map. We also pass it to // fastPoll in order to 'self learn'. We need this because @@ -300,51 +424,60 @@ // its start when it is inverted and a movement key is pressed // (and later restore it again), shouldn't be used for // non-movement keys. - curKeyId = (mod ? "c" : "") + code; - if (sel.inverted && movementKeys.hasOwnProperty(curKeyId)) { + curKeyId = (mod ? "c" : "") + (e.altKey ? "a" : "") + code; + if (sel.inverted && movementKeys[curKeyId] === true) { var range = selRange(input); if (range) { reducedSelection = {anchor: range.start}; setSelRange(input, range.start, range.start); } } + // Don't save the key as a movementkey unless it had a modifier + if (!mod && !e.altKey) curKeyId = null; fastPoll(curKeyId); } function onKeyUp(e) { + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; if (reducedSelection) { reducedSelection = null; updateInput = true; } - if (e.e.keyCode == 16) shiftSelecting = null; + if (e.keyCode == 16) shiftSelecting = null; } function onKeyPress(e) { - if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e.e))) return; + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; if (options.electricChars && mode.electricChars) { - var ch = String.fromCharCode(e.e.charCode == null ? e.e.keyCode : e.e.charCode); + var ch = String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode); if (mode.electricChars.indexOf(ch) > -1) setTimeout(operation(function() {indentLine(sel.to.line, "smart");}), 50); } - var code = e.e.keyCode; + var code = e.keyCode; // Re-stop tab and enter. Necessary on some browsers. - if (code == 13) {handleEnter(); e.stop();} - else if (code == 9 && options.tabMode != "default") e.stop(); + if (code == 13) {if (!options.readOnly) handleEnter(); e_preventDefault(e);} + else if (!e.ctrlKey && !e.altKey && !e.metaKey && code == 9 && options.tabMode != "default") e_preventDefault(e); else fastPoll(curKeyId); } function onFocus() { - if (!focused && options.onFocus) options.onFocus(instance); - focused = true; + if (options.readOnly == "nocursor") return; + if (!focused) { + if (options.onFocus) options.onFocus(instance); + focused = true; + if (wrapper.className.search(/\bCodeMirror-focused\b/) == -1) + wrapper.className += " CodeMirror-focused"; + if (!leaveInputAlone) prepareInput(); + } slowPoll(); - if (wrapper.className.search(/\bCodeMirror-focused\b/) == -1) - wrapper.className += " CodeMirror-focused"; restartBlink(); } function onBlur() { - if (focused && options.onBlur) options.onBlur(instance); + if (focused) { + if (options.onBlur) options.onBlur(instance); + focused = false; + wrapper.className = wrapper.className.replace(" CodeMirror-focused", ""); + } clearInterval(blinker); - shiftSelecting = null; - focused = false; - wrapper.className = wrapper.className.replace(" CodeMirror-focused", ""); + setTimeout(function() {if (!focused) shiftSelecting = null;}, 150); } // Replace the range from from to to by the strings in newText. @@ -367,12 +500,18 @@ var pos = clipPos({line: change.start + change.old.length - 1, ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])}); updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: lines[end-1].text.length}, change.old, pos, pos); + updateInput = true; } } function undo() {unredoHelper(history.done, history.undone);} function redo() {unredoHelper(history.undone, history.done);} function updateLinesNoUndo(from, to, newText, selFrom, selTo) { + var recomputeMaxLength = false, maxLineLength = maxLine.length; + for (var i = from.line; i <= to.line; ++i) { + if (lines[i].text.length == maxLineLength) {recomputeMaxLength = true; break;} + } + var nlines = to.line - from.line, firstLine = lines[from.line], lastLine = lines[to.line]; // First adjust the line structure, taking some care to leave highlighting intact. if (firstLine == lastLine) { @@ -381,24 +520,46 @@ else { lastLine = firstLine.split(to.ch, newText[newText.length-1]); var spliceargs = [from.line + 1, nlines]; - firstLine.replace(from.ch, firstLine.text.length, newText[0]); - for (var i = 1, e = newText.length - 1; i < e; ++i) spliceargs.push(new Line(newText[i])); + firstLine.replace(from.ch, null, newText[0]); + for (var i = 1, e = newText.length - 1; i < e; ++i) + spliceargs.push(Line.inheritMarks(newText[i], firstLine)); spliceargs.push(lastLine); lines.splice.apply(lines, spliceargs); } } else if (newText.length == 1) { - firstLine.replace(from.ch, firstLine.text.length, newText[0] + lastLine.text.slice(to.ch)); + firstLine.replace(from.ch, null, newText[0]); + lastLine.replace(null, to.ch, ""); + firstLine.append(lastLine); lines.splice(from.line + 1, nlines); } else { var spliceargs = [from.line + 1, nlines - 1]; - firstLine.replace(from.ch, firstLine.text.length, newText[0]); - lastLine.replace(0, to.ch, newText[newText.length-1]); - for (var i = 1, e = newText.length - 1; i < e; ++i) spliceargs.push(new Line(newText[i])); + firstLine.replace(from.ch, null, newText[0]); + lastLine.replace(null, to.ch, newText[newText.length-1]); + for (var i = 1, e = newText.length - 1; i < e; ++i) + spliceargs.push(Line.inheritMarks(newText[i], firstLine)); lines.splice.apply(lines, spliceargs); } + + for (var i = from.line, e = i + newText.length; i < e; ++i) { + var l = lines[i].text; + if (l.length > maxLineLength) { + maxLine = l; maxLineLength = l.length; maxWidth = null; + recomputeMaxLength = false; + } + } + if (recomputeMaxLength) { + maxLineLength = 0; maxLine = ""; maxWidth = null; + for (var i = 0, e = lines.length; i < e; ++i) { + var l = lines[i].text; + if (l.length > maxLineLength) { + maxLineLength = l.length; maxLine = l; + } + } + } + // Add these lines to the work array, so that they will be // highlighted. Adjust work lines if lines were added/removed. var newWork = [], lendiff = newText.length - nlines - 1; @@ -407,12 +568,17 @@ if (task < from.line) newWork.push(task); else if (task > to.line) newWork.push(task + lendiff); } - if (newText.length) newWork.push(from.line); + if (newText.length < 5) { + highlightLines(from.line, from.line + newText.length); + newWork.push(from.line + newText.length); + } else { + newWork.push(from.line); + } work = newWork; startWorker(100); // Remember that these lines changed, for updating the display changes.push({from: from.line, to: to.line + 1, diff: lendiff}); - textChanged = true; + textChanged = {from: from, to: to, text: newText}; // Update the selection function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;} @@ -483,7 +649,10 @@ function p() { startOperation(); var changed = readInput(); - if (changed == "moved" && keyId) movementKeys[keyId] = true; + if (changed && keyId) { + if (changed == "moved" && movementKeys[keyId] == null) movementKeys[keyId] = true; + if (changed == "changed") movementKeys[keyId] = false; + } if (!changed && !missed) {missed = true; poll.set(80, p);} else {pollingFast = false; slowPoll();} endOperation(); @@ -495,13 +664,12 @@ // to the data in the editing variable, and updates the editor // content or cursor if something changed. function readInput() { + if (leaveInputAlone || !focused) return; var changed = false, text = input.value, sr = selRange(input); if (!sr) return false; var changed = editing.text != text, rs = reducedSelection; var moved = changed || sr.start != editing.start || sr.end != (rs ? editing.start : editing.end); - if (reducedSelection && !moved && sel.from.line == 0 && sel.from.ch == 0) - reducedSelection = null; - else if (!moved) return false; + if (!moved && !rs) return false; if (changed) { shiftSelecting = reducedSelection = null; if (options.readOnly) {updateInput = true; return "changed";} @@ -524,13 +692,10 @@ // so that you can, for example, press shift-up at the start of // your selection and have the right thing happen. if (rs) { - from = sr.start == rs.anchor ? to : from; - to = shiftSelecting ? sel.to : sr.start == rs.anchor ? from : to; - if (!posLess(from, to)) { - reducedSelection = null; - sel.inverted = false; - var tmp = from; from = to; to = tmp; - } + var head = sr.start == rs.anchor ? to : from; + var tail = shiftSelecting ? sel.to : sr.start == rs.anchor ? from : to; + if (sel.inverted = posLess(head, tail)) { from = head; to = tail; } + else { reducedSelection = null; from = tail; to = head; } } // In some cases (cursor on same line as before), we don't have @@ -550,8 +715,8 @@ var ch = nl > -1 ? start - nl : start, endline = editing.to - 1, edend = editing.text.length; for (;;) { c = editing.text.charAt(edend); + if (text.charAt(end) != c) {++end; ++edend; break;} if (c == "\n") endline--; - if (text.charAt(end) != c) {++end; ++edend; break;} if (edend <= start || end <= start) break; --end; --edend; } @@ -580,22 +745,36 @@ editing = {text: text, from: from, to: to, start: startch, end: endch}; setSelRange(input, startch, reducedSelection ? startch : endch); } + function focusInput() { + if (options.readOnly != "nocursor") input.focus(); + } + function scrollEditorIntoView() { + if (!cursor.getBoundingClientRect) return; + var rect = cursor.getBoundingClientRect(); + var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); + if (rect.top < 0 || rect.bottom > winH) cursor.scrollIntoView(); + } function scrollCursorIntoView() { var cursor = localCoords(sel.inverted ? sel.from : sel.to); return scrollIntoView(cursor.x, cursor.y, cursor.x, cursor.yBot); } function scrollIntoView(x1, y1, x2, y2) { - var pl = paddingLeft(), pt = paddingTop(); + var pl = paddingLeft(), pt = paddingTop(), lh = lineHeight(); y1 += pt; y2 += pt; x1 += pl; x2 += pl; - var screen = wrapper.clientHeight, screentop = wrapper.scrollTop, scrolled = false, result = true; - if (y1 < screentop) {wrapper.scrollTop = Math.max(0, y1 - 10); scrolled = true;} - else if (y2 > screentop + screen) {wrapper.scrollTop = y2 + 10 - screen; scrolled = true;} + var screen = scroller.clientHeight, screentop = scroller.scrollTop, scrolled = false, result = true; + if (y1 < screentop) {scroller.scrollTop = Math.max(0, y1 - 2*lh); scrolled = true;} + else if (y2 > screentop + screen) {scroller.scrollTop = y2 + lh - screen; scrolled = true;} - var screenw = wrapper.clientWidth, screenleft = wrapper.scrollLeft; - if (x1 < screenleft) {wrapper.scrollLeft = Math.max(0, x1 - 10); scrolled = true;} + var screenw = scroller.clientWidth, screenleft = scroller.scrollLeft; + var gutterw = options.fixedGutter ? gutter.clientWidth : 0; + if (x1 < screenleft + gutterw) { + if (x1 < 50) x1 = 0; + scroller.scrollLeft = Math.max(0, x1 - 10 - gutterw); + scrolled = true; + } else if (x2 > screenw + screenleft) { - wrapper.scrollLeft = x2 + 10 - screenw; + scroller.scrollLeft = x2 + 10 - screenw; scrolled = true; if (x2 > code.clientWidth) result = false; } @@ -604,15 +783,15 @@ } function visibleLines() { - var lh = lineHeight(), top = wrapper.scrollTop - paddingTop(); + var lh = lineHeight(), top = scroller.scrollTop - paddingTop(); return {from: Math.min(lines.length, Math.max(0, Math.floor(top / lh))), - to: Math.min(lines.length, Math.ceil((top + wrapper.clientHeight) / lh))}; + to: Math.min(lines.length, Math.ceil((top + scroller.clientHeight) / lh))}; } // Uses a set of changes plus the current scroll position to // determine which DOM updates have to be made, and makes the // updates. function updateDisplay(changes) { - if (!wrapper.clientWidth) { + if (!scroller.clientWidth) { showingFrom = showingTo = 0; return; } @@ -629,7 +808,7 @@ intact2.push(range); else { if (change.from > range.from) - intact2.push({from: range.from, to: change.from, domStart: range.domStart}) + intact2.push({from: range.from, to: change.from, domStart: range.domStart}); if (change.to < range.to) intact2.push({from: change.to + diff, to: range.to + diff, domStart: range.domStart + (change.to - range.from)}); @@ -659,6 +838,7 @@ if (domPos != domEnd || pos != to) { changedLines += Math.abs(to - pos); updates.push({from: pos, to: to, domSize: domEnd - domPos, domStart: domPos}); + if (to - pos != domEnd - domPos) gutterDirty = true; } if (!updates.length) return; @@ -674,13 +854,23 @@ // Position the mover div to align with the lines it's supposed // to be showing (which will cover the visible display) - var different = from != showingFrom || to != showingTo || lastHeight != wrapper.clientHeight; + var different = from != showingFrom || to != showingTo || lastHeight != scroller.clientHeight; showingFrom = from; showingTo = to; mover.style.top = (from * lineHeight()) + "px"; if (different) { - lastHeight = wrapper.clientHeight; + lastHeight = scroller.clientHeight; code.style.height = (lines.length * lineHeight() + 2 * paddingTop()) + "px"; - updateGutter(); + } + if (different || gutterDirty) updateGutter(); + + if (maxWidth == null) maxWidth = stringWidth(maxLine); + if (maxWidth > scroller.clientWidth) { + lineSpace.style.width = maxWidth + "px"; + // Needed to prevent odd wrapping/hiding of widgets placed in here. + code.style.width = ""; + code.style.width = scroller.scrollWidth + "px"; + } else { + lineSpace.style.width = code.style.width = ""; } // Since this is all rather error prone, it is honoured with the @@ -712,7 +902,7 @@ // there .innerHTML on PRE nodes is dumb, and discards // whitespace. var sfrom = sel.from.line, sto = sel.to.line, off = 0, - scratch = badInnerHTML && document.createElement("div"); + scratch = badInnerHTML && targetDocument.createElement("div"); for (var i = 0, e = updates.length; i < e; ++i) { var rec = updates[i]; var extra = (rec.to - rec.from) - rec.domSize; @@ -722,7 +912,7 @@ lineDiv.removeChild(nodeAfter ? nodeAfter.previousSibling : lineDiv.lastChild); else if (extra) { for (var j = Math.max(0, extra); j > 0; --j) - lineDiv.insertBefore(document.createElement("pre"), nodeAfter); + lineDiv.insertBefore(targetDocument.createElement("pre"), nodeAfter); for (var j = Math.max(0, -extra); j > 0; --j) lineDiv.removeChild(nodeAfter ? nodeAfter.previousSibling : lineDiv.lastChild); } @@ -753,10 +943,10 @@ function updateGutter() { if (!options.gutter && !options.lineNumbers) return; - var hText = mover.offsetHeight, hEditor = wrapper.clientHeight; + var hText = mover.offsetHeight, hEditor = scroller.clientHeight; gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px"; var html = []; - for (var i = showingFrom; i < showingTo; ++i) { + for (var i = showingFrom; i < Math.max(showingTo, showingFrom + 1); ++i) { var marker = lines[i].gutterMarker; var text = options.lineNumbers ? i + options.firstLineNumber : null; if (marker && marker.text) @@ -769,37 +959,43 @@ gutterText.innerHTML = html.join(""); var minwidth = String(lines.length).length, firstNode = gutterText.firstChild, val = eltText(firstNode), pad = ""; while (val.length + pad.length < minwidth) pad += "\u00a0"; - if (pad) firstNode.insertBefore(document.createTextNode(pad), firstNode.firstChild); + if (pad) firstNode.insertBefore(targetDocument.createTextNode(pad), firstNode.firstChild); gutter.style.display = ""; lineSpace.style.marginLeft = gutter.offsetWidth + "px"; + gutterDirty = false; } function updateCursor() { - var head = sel.inverted ? sel.from : sel.to; - var x = charX(head.line, head.ch) + "px", y = (head.line - showingFrom) * lineHeight() + "px"; - inputDiv.style.top = y; inputDiv.style.left = x; + var head = sel.inverted ? sel.from : sel.to, lh = lineHeight(); + var x = charX(head.line, head.ch); + var top = head.line * lh - scroller.scrollTop; + inputDiv.style.top = Math.max(Math.min(top, scroller.offsetHeight), 0) + "px"; + inputDiv.style.left = (x - scroller.scrollLeft) + "px"; if (posEq(sel.from, sel.to)) { - cursor.style.top = y; cursor.style.left = x; + cursor.style.top = (head.line - showingFrom) * lh + "px"; + cursor.style.left = x + "px"; cursor.style.display = ""; } else cursor.style.display = "none"; } + function setSelectionUser(from, to) { + var sh = shiftSelecting && clipPos(shiftSelecting); + if (sh) { + if (posLess(sh, from)) from = sh; + else if (posLess(to, sh)) to = sh; + } + setSelection(from, to); + } // Update the selection. Last two args are only used by // updateLines, since they have to be expressed in the line // numbers before the update. function setSelection(from, to, oldFrom, oldTo) { if (posEq(sel.from, from) && posEq(sel.to, to)) return; - var sh = shiftSelecting && clipPos(shiftSelecting); if (posLess(to, from)) {var tmp = to; to = from; from = tmp;} - if (sh) { - if (posLess(sh, from)) from = sh; - else if (posLess(to, sh)) to = sh; - } - var startEq = posEq(sel.to, to), endEq = posEq(sel.from, from); if (posEq(from, to)) sel.inverted = false; - else if (startEq && !endEq) sel.inverted = true; - else if (endEq && !startEq) sel.inverted = false; + else if (posEq(from, sel.to)) sel.inverted = false; + else if (posEq(to, sel.from)) sel.inverted = true; // Some ugly logic used to only mark the lines that actually did // see a change in selection as changed, rather than the whole @@ -829,9 +1025,9 @@ sel.from = from; sel.to = to; selectionChanged = true; } - function setCursor(line, ch) { + function setCursor(line, ch, user) { var pos = clipPos({line: line, ch: ch || 0}); - setSelection(pos, pos); + (user ? setSelectionUser : setSelection)(pos, pos); } function clipLine(n) {return Math.max(0, Math.min(n, lines.length-1));} @@ -845,11 +1041,12 @@ } function scrollPage(down) { - var linesPerPage = Math.floor(wrapper.clientHeight / lineHeight()), head = sel.inverted ? sel.from : sel.to; - setCursor(head.line + (Math.max(linesPerPage - 1, 1) * (down ? 1 : -1)), head.ch); + var linesPerPage = Math.floor(scroller.clientHeight / lineHeight()), head = sel.inverted ? sel.from : sel.to; + setCursor(head.line + (Math.max(linesPerPage - 1, 1) * (down ? 1 : -1)), head.ch, true); } function scrollEnd(top) { - setCursor(top ? 0 : lines.length - 1); + var pos = top ? {line: 0, ch: 0} : {line: lines.length - 1, ch: lines[lines.length-1].text.length}; + setSelectionUser(pos, pos); } function selectAll() { var endLine = lines.length - 1; @@ -859,8 +1056,11 @@ var line = lines[pos.line].text; var start = pos.ch, end = pos.ch; while (start > 0 && /\w/.test(line.charAt(start - 1))) --start; - while (end < line.length - 1 && /\w/.test(line.charAt(end))) ++end; - setSelection({line: pos.line, ch: start}, {line: pos.line, ch: end}); + while (end < line.length && /\w/.test(line.charAt(end))) ++end; + setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end}); + } + function selectLine(line) { + setSelectionUser({line: line, ch: 0}, {line: line, ch: lines[line].text.length}); } function handleEnter() { replaceSelection("\n", "end"); @@ -868,12 +1068,17 @@ indentLine(sel.from.line, options.enterMode == "keep" ? "prev" : "smart"); } function handleTab(shift) { + function indentSelected(mode) { + if (posEq(sel.from, sel.to)) return indentLine(sel.from.line, mode); + var e = sel.to.line - (sel.to.ch ? 0 : 1); + for (var i = sel.from.line; i <= e; ++i) indentLine(i, mode); + } shiftSelecting = null; switch (options.tabMode) { case "default": return false; case "indent": - for (var i = sel.from.line, e = sel.to.line; i <= e; ++i) indentLine(i, "smart"); + indentSelected("smart"); break; case "classic": if (posEq(sel.from, sel.to)) { @@ -882,11 +1087,15 @@ break; } case "shift": - for (var i = sel.from.line, e = sel.to.line; i <= e; ++i) indentLine(i, shift ? "subtract" : "add"); + indentSelected(shift ? "subtract" : "add"); break; } return true; } + function smartHome() { + var firstNonWS = Math.max(0, lines[sel.from.line].text.search(/\S/)); + setCursor(sel.from.line, sel.from.ch <= firstNonWS && sel.from.ch ? 0 : firstNonWS, true); + } function indentLine(n, how) { if (how == "smart") { @@ -924,21 +1133,20 @@ for (var i = 0, l = lines.length; i < l; ++i) lines[i].stateAfter = null; work = [0]; + startWorker(); } function gutterChanged() { var visible = options.gutter || options.lineNumbers; gutter.style.display = visible ? "" : "none"; - if (visible) updateGutter(); + if (visible) gutterDirty = true; else lineDiv.parentNode.style.marginLeft = 0; } function markText(from, to, className) { from = clipPos(from); to = clipPos(to); - var accum = []; + var set = []; function add(line, from, to, className) { - var line = lines[line], mark = line.addMark(from, to, className); - mark.line = line; - accum.push(mark); + mark = lines[line].addMark(from, to, className, set); } if (from.line == to.line) add(from.line, from.ch, to.ch, className); else { @@ -948,30 +1156,51 @@ add(to.line, 0, to.ch, className); } changes.push({from: from.line, to: to.line + 1}); - return function() { - var start, end; - for (var i = 0; i < accum.length; ++i) { - var mark = accum[i], found = indexOf(lines, mark.line); - mark.line.removeMark(mark); - if (found > -1) { - if (start == null) start = found; - end = found; + return new TextMarker(set); + } + + function TextMarker(set) { this.set = set; } + TextMarker.prototype.clear = operation(function() { + for (var i = 0, e = this.set.length; i < e; ++i) { + var mk = this.set[i].marked; + for (var j = 0; j < mk.length; ++j) { + if (mk[j].set == this.set) mk.splice(j--, 1); + } + } + // We don't know the exact lines that changed. Refreshing is + // cheaper than finding them. + changes.push({from: 0, to: lines.length}); + }); + TextMarker.prototype.find = function() { + var from, to; + for (var i = 0, e = this.set.length; i < e; ++i) { + var line = this.set[i], mk = line.marked; + for (var j = 0; j < mk.length; ++j) { + var mark = mk[j]; + if (mark.set == this.set) { + if (mark.from != null || mark.to != null) { + var found = indexOf(lines, line); + if (found > -1) { + if (mark.from != null) from = {line: found, ch: mark.from}; + if (mark.to != null) to = {line: found, ch: mark.to}; + } + } } } - if (start != null) changes.push({from: start, to: end + 1}); - }; - } + } + return {from: from, to: to}; + }; function addGutterMarker(line, text, className) { if (typeof line == "number") line = lines[clipLine(line)]; line.gutterMarker = {text: text, style: className}; - updateGutter(); + gutterDirty = true; return line; } function removeGutterMarker(line) { if (typeof line == "number") line = lines[clipLine(line)]; line.gutterMarker = null; - updateGutter(); + gutterDirty = true; } function setLineClass(line, className) { if (typeof line == "number") { @@ -982,8 +1211,10 @@ var no = indexOf(lines, line); if (no == -1) return null; } - line.className = className; - changes.push({from: no, to: no + 1}); + if (line.className != className) { + line.className = className; + changes.push({from: no, to: no + 1}); + } return line; } @@ -1001,35 +1232,44 @@ return {line: n, text: line.text, markerText: marker && marker.text, markerClass: marker && marker.style}; } + function stringWidth(str) { + measure.innerHTML = "<pre><span>x</span></pre>"; + measure.firstChild.firstChild.firstChild.nodeValue = str; + return measure.firstChild.firstChild.offsetWidth || 10; + } // These are used to go from pixel positions to character - // positions, taking tabs into account. + // positions, taking varying character widths into account. function charX(line, pos) { - var text = lines[line].text, span = measure.firstChild; - if (text.lastIndexOf("\t", pos) == -1) return pos * charWidth(); - var old = span.firstChild.nodeValue; - try { - span.firstChild.nodeValue = text.slice(0, pos); - return span.offsetWidth; - } finally {span.firstChild.nodeValue = old;} + if (pos == 0) return 0; + measure.innerHTML = "<pre><span>" + lines[line].getHTML(null, null, false, pos) + "</span></pre>"; + return measure.firstChild.firstChild.offsetWidth; } function charFromX(line, x) { - var text = lines[line].text, cw = charWidth(); if (x <= 0) return 0; - if (text.indexOf("\t") == -1) return Math.min(text.length, Math.round(x / cw)); - var mspan = measure.firstChild, mtext = mspan.firstChild, old = mtext.nodeValue; - try { - mtext.nodeValue = text; - var from = 0, fromX = 0, to = text.length, toX = mspan.offsetWidth; - if (x > toX) return to; - for (;;) { - if (to - from <= 1) return (toX - x > x - fromX) ? from : to; - var middle = Math.ceil((from + to) / 2); - mtext.nodeValue = text.slice(0, middle); - var curX = mspan.offsetWidth; - if (curX > x) {to = middle; toX = curX;} - else {from = middle; fromX = curX;} - } - } finally {mtext.nodeValue = old;} + var lineObj = lines[line], text = lineObj.text; + function getX(len) { + measure.innerHTML = "<pre><span>" + lineObj.getHTML(null, null, false, len) + "</span></pre>"; + return measure.firstChild.firstChild.offsetWidth; + } + var from = 0, fromX = 0, to = text.length, toX; + // Guess a suitable upper bound for our search. + var estimated = Math.min(to, Math.ceil(x / stringWidth("x"))); + for (;;) { + var estX = getX(estimated); + if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); + else {toX = estX; to = estimated; break;} + } + if (x > toX) return to; + // Try to guess a suitable lower bound as well. + estimated = Math.floor(to * 0.8); estX = getX(estimated); + if (estX < x) {from = estimated; fromX = estX;} + // Do a binary search between these bounds. + for (;;) { + if (to - from <= 1) return (toX - x > x - fromX) ? from : to; + var middle = Math.ceil((from + to) / 2), middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX;} + else {from = middle; fromX = middleX;} + } } function localCoords(pos, inLineWrap) { @@ -1043,45 +1283,61 @@ function lineHeight() { var nlines = lineDiv.childNodes.length; - if (nlines) return lineDiv.offsetHeight / nlines; - else return measure.firstChild.offsetHeight || 1; + if (nlines) return (lineDiv.offsetHeight / nlines) || 1; + measure.innerHTML = "<pre>x</pre>"; + return measure.firstChild.offsetHeight || 1; } - function charWidth() {return (measure.firstChild.offsetWidth || 320) / 40;} function paddingTop() {return lineSpace.offsetTop;} function paddingLeft() {return lineSpace.offsetLeft;} function posFromMouse(e, liberal) { - var off = eltOffset(lineSpace), - x = e.pageX() - off.left, - y = e.pageY() - off.top; - if (!liberal && e.target() != lineSpace.parentNode && !(e.target() == wrapper && y > (lines.length * lineHeight()))) - for (var n = e.target(); n != lineDiv && n != cursor; n = n.parentNode) - if (!n || n == wrapper) return null; - var line = showingFrom + Math.floor(y / lineHeight()); - return clipPos({line: line, ch: charFromX(clipLine(line), x)}); + var offW = eltOffset(scroller, true), x, y; + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX; y = e.clientY; } catch (e) { return null; } + // This is a mess of a heuristic to try and determine whether a + // scroll-bar was clicked or not, and to return null if one was + // (and !liberal). + if (!liberal && (x - offW.left > scroller.clientWidth || y - offW.top > scroller.clientHeight)) + return null; + var offL = eltOffset(lineSpace, true); + var line = showingFrom + Math.floor((y - offL.top) / lineHeight()); + return clipPos({line: line, ch: charFromX(clipLine(line), x - offL.left)}); } function onContextMenu(e) { var pos = posFromMouse(e); if (!pos || window.opera) return; // Opera is difficult. if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to)) - setCursor(pos.line, pos.ch); + operation(setCursor)(pos.line, pos.ch); var oldCSS = input.style.cssText; - input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.pageY() - 1) + - "px; left: " + (e.pageX() - 1) + "px; z-index: 1000; background: white; " + - "border-width: 0; outline: none; overflow: hidden;"; + inputDiv.style.position = "absolute"; + input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " + + "border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + leaveInputAlone = true; var val = input.value = getSelection(); - input.focus(); - setSelRange(input, 0, val.length); - if (gecko) e.stop(); - leaveInputAlone = true; - setTimeout(function() { - if (input.value != val) operation(replaceSelection)(input.value, "end"); + focusInput(); + setSelRange(input, 0, input.value.length); + function rehide() { + var newVal = splitLines(input.value).join("\n"); + if (newVal != val) operation(replaceSelection)(newVal, "end"); + inputDiv.style.position = "relative"; input.style.cssText = oldCSS; leaveInputAlone = false; prepareInput(); slowPoll(); - }, 50); + } + + if (gecko) { + e_stop(e); + var mouseup = connect(window, "mouseup", function() { + mouseup(); + setTimeout(rehide, 20); + }, true); + } + else { + setTimeout(rehide, 50); + } } // Cursor-blinking @@ -1120,19 +1376,18 @@ } } } - for (var i = head.line, e = forward ? Math.min(i + 50, lines.length) : Math.max(0, i - 50); i != e; i+=d) { + for (var i = head.line, e = forward ? Math.min(i + 100, lines.length) : Math.max(-1, i - 100); i != e; i+=d) { var line = lines[i], first = i == head.line; var found = scan(line, first && forward ? pos + 1 : 0, first && !forward ? pos : line.text.length); - if (found) { - var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; - var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style), - two = markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style); - var clear = operation(function(){one(); two();}); - if (autoclear) setTimeout(clear, 800); - else bracketHighlighted = clear; - break; - } + if (found) break; } + if (!found) found = {pos: null, match: false}; + var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style), + two = found.pos != null && markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style); + var clear = operation(function(){one.clear(); two && two.clear();}); + if (autoclear) setTimeout(clear, 800); + else bracketHighlighted = clear; } // Finds the line to start with when starting a parse. Tries to @@ -1148,7 +1403,7 @@ if (line.stateAfter) return search; var indented = line.indentation(); if (minline == null || minindent > indented) { - minline = search; + minline = search - 1; minindent = indented; } } @@ -1163,11 +1418,21 @@ line.highlight(mode, state); line.stateAfter = copyState(mode, state); } - if (!lines[n].stateAfter) work.push(n); + changes.push({from: start, to: n}); + if (n < lines.length && !lines[n].stateAfter) work.push(n); return state; } + function highlightLines(start, end) { + var state = getStateBefore(start); + for (var i = start; i < end; ++i) { + var line = lines[i]; + line.highlight(mode, state); + line.stateAfter = copyState(mode, state); + } + } function highlightWorker() { var end = +new Date + options.workTime; + var foundWork = work.length; while (work.length) { if (!lines[showingFrom].stateAfter) var task = showingFrom; else var task = work.pop(); @@ -1176,20 +1441,29 @@ if (state) state = copyState(mode, state); else state = startState(mode); + var unchanged = 0, compare = mode.compareStates, realChange = false; for (var i = start, l = lines.length; i < l; ++i) { var line = lines[i], hadState = line.stateAfter; if (+new Date > end) { work.push(i); startWorker(options.workDelay); - changes.push({from: task, to: i}); + if (realChange) changes.push({from: task, to: i + 1}); return; } var changed = line.highlight(mode, state); + if (changed) realChange = true; line.stateAfter = copyState(mode, state); - if (hadState && !changed && line.text) break; + if (compare) { + if (hadState && compare(hadState, state)) break; + } else { + if (changed !== false || !hadState) unchanged = 0; + else if (++unchanged > 3) break; + } } - changes.push({from: task, to: i}); + if (realChange) changes.push({from: task, to: i + 1}); } + if (foundWork && options.onHighlightComplete) + options.onHighlightComplete(instance); } function startWorker(time) { if (!work.length) return; @@ -1207,24 +1481,29 @@ var reScroll = false; if (selectionChanged) reScroll = !scrollCursorIntoView(); if (changes.length) updateDisplay(changes); - else if (selectionChanged) updateCursor(); + else { + if (selectionChanged) updateCursor(); + if (gutterDirty) updateGutter(); + } if (reScroll) scrollCursorIntoView(); - if (selectionChanged) restartBlink(); + if (selectionChanged) {scrollEditorIntoView(); restartBlink();} // updateInput can be set to a boolean value to force/prevent an // update. - if (!leaveInputAlone && (updateInput === true || (updateInput !== false && selectionChanged))) + if (focused && !leaveInputAlone && + (updateInput === true || (updateInput !== false && selectionChanged))) prepareInput(); - if (selectionChanged && options.onCursorActivity) - options.onCursorActivity(instance); - if (textChanged && options.onChange) - options.onChange(instance); if (selectionChanged && options.matchBrackets) setTimeout(operation(function() { if (bracketHighlighted) {bracketHighlighted(); bracketHighlighted = null;} matchBrackets(false); }), 20); + var tc = textChanged; // textChanged can be reset by cursoractivity callback + if (selectionChanged && options.onCursorActivity) + options.onCursorActivity(instance); + if (tc && options.onChange && instance) + options.onChange(instance, tc); } var nestedOperation = 0; function operation(f) { @@ -1259,6 +1538,7 @@ var newmatch = line.match(query); if (newmatch) match = newmatch; else break; + start++; } } else { @@ -1338,9 +1618,21 @@ }, from: function() {if (this.atOccurrence) return copyPos(this.pos.from);}, - to: function() {if (this.atOccurrence) return copyPos(this.pos.to);} + to: function() {if (this.atOccurrence) return copyPos(this.pos.to);}, + + replace: function(newText) { + var self = this; + if (this.atOccurrence) + operation(function() { + self.pos.to = replaceRange(newText, self.pos.from, self.pos.to); + })(); + } }; + for (var ext in extensions) + if (extensions.propertyIsEnumerable(ext) && + !instance.propertyIsEnumerable(ext)) + instance[ext] = extensions[ext]; return instance; } // (end of function CodeMirror) @@ -1348,6 +1640,7 @@ CodeMirror.defaults = { value: "", mode: null, + theme: "default", indentUnit: 2, indentWithTabs: false, tabMode: "classic", @@ -1356,17 +1649,21 @@ onKeyEvent: null, lineNumbers: false, gutter: false, + fixedGutter: false, firstLineNumber: 1, readOnly: false, + smartHome: true, onChange: null, onCursorActivity: null, onGutterClick: null, + onHighlightComplete: null, onFocus: null, onBlur: null, onScroll: null, matchBrackets: false, workTime: 100, workDelay: 200, undoDepth: 40, - tabindex: null + tabindex: null, + document: window.document }; // Known modes, by name and by MIME @@ -1383,15 +1680,15 @@ spec = mimeModes[spec]; if (typeof spec == "string") var mname = spec, config = {}; - else + else if (spec != null) var mname = spec.name, config = spec; var mfactory = modes[mname]; if (!mfactory) { if (window.console) console.warn("No mode " + mname + " found, falling back to plain text."); return CodeMirror.getMode(options, "text/plain"); } - return mfactory(options, config); - } + return mfactory(options, config || {}); + }; CodeMirror.listModes = function() { var list = []; for (var m in modes) @@ -1401,10 +1698,15 @@ CodeMirror.listMIMEs = function() { var list = []; for (var m in mimeModes) - if (mimeModes.propertyIsEnumerable(m)) list.push(m); + if (mimeModes.propertyIsEnumerable(m)) list.push({mime: m, mode: mimeModes[m]}); return list; }; + var extensions = {}; + CodeMirror.defineExtension = function(name, func) { + extensions[name] = func; + }; + CodeMirror.fromTextArea = function(textarea, options) { if (!options) options = {}; options.value = textarea.value; @@ -1484,7 +1786,7 @@ if (ok) {++this.pos; return ch;} }, eatWhile: function(match) { - var start = this.start; + var start = this.pos; while (this.eat(match)){} return this.pos > start; }, @@ -1517,6 +1819,7 @@ }, current: function(){return this.string.slice(this.start, this.pos);} }; + CodeMirror.StringStream = StringStream; // Line objects. These hold state related to a line, including // highlighting info (the styles array). @@ -1526,10 +1829,23 @@ this.text = text; this.marked = this.gutterMarker = this.className = null; } + Line.inheritMarks = function(text, orig) { + var ln = new Line(text), mk = orig.marked; + if (mk) { + for (var i = 0; i < mk.length; ++i) { + if (mk[i].to == null) { + var newmk = ln.marked || (ln.marked = []), mark = mk[i]; + newmk.push({from: null, to: null, style: mark.style, set: mark.set}); + mark.set.push(ln); + } + } + } + return ln; + } Line.prototype = { // Replace a piece of a line, keeping the styles around it intact. - replace: function(from, to, text) { - var st = [], mk = this.marked; + replace: function(from, to_, text) { + var st = [], mk = this.marked, to = to_ == null ? this.text.length : to_; copyStyles(0, from, this.styles, st); if (text) st.push(text, null); copyStyles(to, this.text.length, this.styles, st); @@ -1538,39 +1854,86 @@ this.stateAfter = null; if (mk) { var diff = text.length - (to - from), end = this.text.length; - function fix(n) {return n <= Math.min(to, to + diff) ? n : n + diff;} + var changeStart = Math.min(from, from + diff); for (var i = 0; i < mk.length; ++i) { var mark = mk[i], del = false; - if (mark.from >= end) del = true; - else {mark.from = fix(mark.from); if (mark.to != null) mark.to = fix(mark.to);} - if (del || mark.from >= mark.to) {mk.splice(i, 1); i--;} + if (mark.from != null && mark.from >= end) del = true; + else { + if (mark.from != null && mark.from >= from) { + mark.from += diff; + if (mark.from <= 0) mark.from = from == null ? null : 0; + } + else if (to_ == null) mark.to = null; + if (mark.to != null && mark.to > from) { + mark.to += diff; + if (mark.to < 0) del = true; + } + } + if (del || (mark.from != null && mark.to != null && mark.from >= mark.to)) mk.splice(i--, 1); } } }, - // Split a line in two, again keeping styles intact. + // Split a part off a line, keeping styles and markers intact. split: function(pos, textBefore) { - var st = [textBefore, null]; + var st = [textBefore, null], mk = this.marked; copyStyles(pos, this.text.length, this.styles, st); - return new Line(textBefore + this.text.slice(pos), st); + var taken = new Line(textBefore + this.text.slice(pos), st); + if (mk) { + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + if (mark.to > pos || mark.to == null) { + if (!taken.marked) taken.marked = []; + taken.marked.push({ + from: mark.from < pos || mark.from == null ? null : mark.from - pos + textBefore.length, + to: mark.to == null ? null : mark.to - pos + textBefore.length, + style: mark.style, set: mark.set + }); + mark.set.push(taken); + } + } + } + return taken; }, - addMark: function(from, to, style) { - var mk = this.marked, mark = {from: from, to: to, style: style}; + append: function(line) { + if (!line.text.length) return; + var mylen = this.text.length, mk = line.marked; + this.text += line.text; + copyStyles(0, line.text.length, line.styles, this.styles); + if (mk && mk.length) { + var mymk = this.marked || (this.marked = []); + for (var i = 0; i < mymk.length; ++i) + if (mymk[i].to == null) mymk[i].to = mylen; + outer: for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + if (!mark.from) { + for (var j = 0; j < mymk.length; ++j) { + var mymark = mymk[j]; + if (mymark.to == mylen && mymark.set == mark.set) { + mymark.to = mark.to == null ? null : mark.to + mylen; + continue outer; + } + } + } + mymk.push(mark); + mark.set.push(this); + mark.from += mylen; + if (mark.to != null) mark.to += mylen; + } + } + }, + addMark: function(from, to, style, set) { + set.push(this); if (this.marked == null) this.marked = []; - this.marked.push(mark); - this.marked.sort(function(a, b){return a.from - b.from;}); - return mark; - }, - removeMark: function(mark) { - var mk = this.marked; - if (!mk) return; - for (var i = 0; i < mk.length; ++i) - if (mk[i] == mark) {mk.splice(i, 1); break;} + this.marked.push({from: from, to: to, style: style, set: set}); + this.marked.sort(function(a, b){return (a.from || 0) - (b.from || 0);}); }, // Run the given mode's parser over a line, update the styles // array, which contains alternating fragments of text and CSS // classes. highlight: function(mode, state) { - var stream = new StringStream(this.text), st = this.styles, pos = 0, changed = false; + var stream = new StringStream(this.text), st = this.styles, pos = 0; + var changed = false, curWord = st[0], prevWord; + if (this.text == "" && mode.blankLine) mode.blankLine(state); while (!stream.eol()) { var style = mode.token(stream, state); var substr = this.text.slice(stream.start, stream.pos); @@ -1578,8 +1941,9 @@ if (pos && st[pos-1] == style) st[pos-2] += substr; else if (substr) { - if (!changed && st[pos] != substr || st[pos+1] != style) changed = true; + if (!changed && (st[pos+1] != style || (pos && st[pos-2] != prevWord))) changed = true; st[pos++] = substr; st[pos++] = style; + prevWord = curWord; curWord = st[pos]; } // Give up when line is ridiculously long if (stream.pos > 5000) { @@ -1588,7 +1952,11 @@ } } if (st.length != pos) {st.length = pos; changed = true;} - return changed; + if (pos && st[pos-2] != prevWord) changed = true; + // Short lines with simple highlights return null, and are + // counted as changed by the driver because they are likely to + // highlight the same way in various contexts. + return changed || (st.length < 5 && this.text.length < 10 ? null : false); }, // Fetch the parser token for a given character. Useful for hacks // that want to inspect the mode state (say, for completion). @@ -1607,7 +1975,7 @@ indentation: function() {return countColumn(this.text);}, // Produces an HTML fragment for the line, taking selection, // marking, and highlighting into account. - getHTML: function(sfrom, sto, includePre) { + getHTML: function(sfrom, sto, includePre, endAt) { var html = []; if (includePre) html.push(this.className ? '<pre class="' + this.className + '">': "<pre>"); @@ -1618,11 +1986,18 @@ } var st = this.styles, allText = this.text, marked = this.marked; if (sfrom == sto) sfrom = null; + var len = allText.length; + if (endAt != null) len = Math.min(endAt, len); - if (!allText) + if (!allText && endAt == null) span(" ", sfrom != null && sto == null ? "CodeMirror-selected" : null); else if (!marked && sfrom == null) - for (var i = 0, e = st.length; i < e; i+=2) span(st[i], st[i+1]); + for (var i = 0, ch = 0; ch < len; i+=2) { + var str = st[i], style = st[i+1], l = str.length; + if (ch + l > len) str = str.slice(0, len - ch); + ch += l; + span(str, style && "cm-" + style); + } else { var pos = 0, i = 0, text = "", style, sg = 0; var markpos = -1, mark = null; @@ -1632,9 +2007,9 @@ mark = (markpos < marked.length) ? marked[markpos] : null; } } - nextMark(); - while (pos < allText.length) { - var upto = allText.length; + nextMark(); + while (pos < len) { + var upto = len; var extraStyle = ""; if (sfrom != null) { if (sfrom > pos) upto = sfrom; @@ -1653,12 +2028,12 @@ } for (;;) { var end = pos + text.length; - var apliedStyle = style; - if (extraStyle) apliedStyle = style ? style + extraStyle : extraStyle; - span(end > upto ? text.slice(0, upto - pos) : text, apliedStyle); + var appliedStyle = style; + if (extraStyle) appliedStyle = style ? style + extraStyle : extraStyle; + span(end > upto ? text.slice(0, upto - pos) : text, appliedStyle); if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} pos = end; - text = st[i++]; style = st[i++]; + text = st[i++]; style = "cm-" + st[i++]; } } if (sfrom != null && sto == null) span(" ", "CodeMirror-selected"); @@ -1716,42 +2091,34 @@ } }; - // Event stopping compatibility wrapper. - function stopEvent() { - if (this.preventDefault) {this.preventDefault(); this.stopPropagation();} - else {this.returnValue = false; this.cancelBubble = true;} - } + function stopMethod() {e_stop(this);} // Ensure an event has a stop method. function addStop(event) { - if (!event.stop) event.stop = stopEvent; + if (!event.stop) event.stop = stopMethod; return event; } - // Event wrapper, exposing the few operations we need. - function Event(orig) {this.e = orig;} - Event.prototype = { - stop: function() {stopEvent.call(this.e);}, - target: function() {return this.e.target || this.e.srcElement;}, - button: function() { - if (this.e.which) return this.e.which; - else if (this.e.button & 1) return 1; - else if (this.e.button & 2) return 3; - else if (this.e.button & 4) return 2; - }, - pageX: function() { - if (this.e.pageX != null) return this.e.pageX; - else return this.e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; - }, - pageY: function() { - if (this.e.pageY != null) return this.e.pageY; - else return this.e.clientY + document.body.scrollTop + document.documentElement.scrollTop; - } - }; + function e_preventDefault(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + } + function e_stopPropagation(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + } + function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + if (e.which) return e.which; + else if (e.button & 1) return 1; + else if (e.button & 2) return 3; + else if (e.button & 4) return 2; + } // Event handler registration. If disconnect is true, it'll return a // function that unregisters the handler. function connect(node, type, handler, disconnect) { - function wrapHandler(event) {handler(new Event(event || window.event));} + function wrapHandler(event) {handler(event || window.event);} if (typeof node.addEventListener == "function") { node.addEventListener(type, wrapHandler, false); if (disconnect) return function() {node.removeEventListener(type, wrapHandler, false);}; @@ -1772,7 +2139,18 @@ pre.innerHTML = " "; return !pre.innerHTML; })(); + // Detect drag-and-drop + var dragAndDrop = (function() { + // IE8 has ondragstart and ondrop properties, but doesn't seem to + // actually support ondragstart the way it's supposed to work. + if (/MSIE [1-8]\b/.test(navigator.userAgent)) return false; + var div = document.createElement('div'); + return "ondragstart" in div && "ondrop" in div; + })(); + var gecko = /gecko\/\d{7}/i.test(navigator.userAgent); + var ie = /MSIE \d/.test(navigator.userAgent); + var safari = /Apple Computer/.test(navigator.vendor); var lineSep = "\n"; // Feature-detect whether newlines in textareas are converted to \r\n @@ -1802,11 +2180,23 @@ return n; } + function computedStyle(elt) { + if (elt.currentStyle) return elt.currentStyle; + return window.getComputedStyle(elt, null); + } // Find the position of an element by following the offsetParent chain. - function eltOffset(node) { - var x = 0, y = 0, n2 = node; - for (var n = node; n; n = n.offsetParent) {x += n.offsetLeft; y += n.offsetTop;} - for (var n = node; n != document.body; n = n.parentNode) {x -= n.scrollLeft; y -= n.scrollTop;} + // If screen==true, it returns screen (rather than page) coordinates. + function eltOffset(node, screen) { + var doc = node.ownerDocument.body; + var x = 0, y = 0, skipDoc = false; + for (var n = node; n; n = n.offsetParent) { + x += n.offsetLeft; y += n.offsetTop; + if (screen && computedStyle(n).position == "fixed") + skipDoc = true; + } + var e = screen && !skipDoc ? null : doc; + for (var n = node.parentNode; n != e; n = n.parentNode) + if (n.scrollLeft != null) { x -= n.scrollLeft; y -= n.scrollTop;} return {left: x, top: y}; } // Get a node's text content. @@ -1819,9 +2209,18 @@ function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);} function copyPos(x) {return {line: x.line, ch: x.ch};} + var escapeElement = document.createElement("pre"); function htmlEscape(str) { - return str.replace(/[<&]/g, function(str) {return str == "&" ? "&" : "<";}); + if (badTextContent) { + escapeElement.innerHTML = ""; + escapeElement.appendChild(document.createTextNode(str)); + } else { + escapeElement.textContent = str; + } + return escapeElement.innerHTML; } + var badTextContent = htmlEscape("\t") != "\t"; + CodeMirror.htmlEscape = htmlEscape; // Used to position the cursor after an undo/redo by finding the // last edited character. @@ -1842,8 +2241,9 @@ // See if "".split is the broken IE version, if so, provide an // alternative way to split lines. + var splitLines, selRange, setSelRange; if ("\n\nb".split(/\n/).length != 3) - var splitLines = function(string) { + splitLines = function(string) { var pos = 0, nl, result = []; while ((nl = string.indexOf("\n", pos)) > -1) { result.push(string.slice(pos, string.charAt(nl-1) == "\r" ? nl - 1 : nl)); @@ -1853,23 +2253,40 @@ return result; }; else - var splitLines = function(string){return string.split(/\r?\n/);}; + splitLines = function(string){return string.split(/\r?\n/);}; + CodeMirror.splitLines = splitLines; // Sane model of finding and setting the selection in a textarea if (window.getSelection) { - var selRange = function(te) { + selRange = function(te) { try {return {start: te.selectionStart, end: te.selectionEnd};} catch(e) {return null;} }; - var setSelRange = function(te, start, end) { - try {te.setSelectionRange(start, end);} - catch(e) {} // Fails on Firefox when textarea isn't part of the document - }; + if (safari) + // On Safari, selection set with setSelectionRange are in a sort + // of limbo wrt their anchor. If you press shift-left in them, + // the anchor is put at the end, and the selection expanded to + // the left. If you press shift-right, the anchor ends up at the + // front. This is not what CodeMirror wants, so it does a + // spurious modify() call to get out of limbo. + setSelRange = function(te, start, end) { + if (start == end) + te.setSelectionRange(start, end); + else { + te.setSelectionRange(start, end - 1); + window.getSelection().modify("extend", "forward", "character"); + } + }; + else + setSelRange = function(te, start, end) { + try {te.setSelectionRange(start, end);} + catch(e) {} // Fails on Firefox when textarea isn't part of the document + }; } // IE model. Don't ask. else { - var selRange = function(te) { - try {var range = document.selection.createRange();} + selRange = function(te) { + try {var range = te.ownerDocument.selection.createRange();} catch(e) {return null;} if (!range || range.parentElement() != te) return null; var val = te.value, len = val.length, localRange = te.createTextRange(); @@ -1890,7 +2307,7 @@ for (var i = val.indexOf("\r"); i > -1 && i < end; i = val.indexOf("\r", i+1), end++) {} return {start: start, end: end}; }; - var setSelRange = function(te, start, end) { + setSelRange = function(te, start, end) { var range = te.createTextRange(); range.collapse(true); var endrange = range.duplicate();