Skip to content
This repository
Browse code

CodeMirror code editing added.

* codemirror2 repo has been added to IPython.
* Custom coloring used to match the qtconsole.
  • Loading branch information...
commit f5911d3ce590e2750f3e1210b0b398f5faff2990 1 parent e3dc236
Brian E. Granger ellisonbg authored

Showing 16 changed files with 3,693 additions and 31 deletions. Show diff stats Hide diff stats

  1. +19 0 IPython/frontend/html/notebook/static/codemirror2/LICENSE
  2. +58 0 IPython/frontend/html/notebook/static/codemirror2/lib/codemirror.css
  3. +1,985 0 IPython/frontend/html/notebook/static/codemirror2/lib/codemirror.js
  4. +51 0 IPython/frontend/html/notebook/static/codemirror2/lib/overlay.js
  5. +74 0 IPython/frontend/html/notebook/static/codemirror2/mode/htmlmixed/htmlmixed.js
  6. +54 0 IPython/frontend/html/notebook/static/codemirror2/mode/htmlmixed/index.html
  7. +21 0 IPython/frontend/html/notebook/static/codemirror2/mode/python/LICENSE.txt
  8. +123 0 IPython/frontend/html/notebook/static/codemirror2/mode/python/index.html
  9. +23 0 IPython/frontend/html/notebook/static/codemirror2/mode/python/python.css
  10. +318 0 IPython/frontend/html/notebook/static/codemirror2/mode/python/python.js
  11. +526 0 IPython/frontend/html/notebook/static/codemirror2/mode/rst/index.html
  12. +77 0 IPython/frontend/html/notebook/static/codemirror2/mode/rst/rst.css
  13. +335 0 IPython/frontend/html/notebook/static/codemirror2/mode/rst/rst.js
  14. +2 13 IPython/frontend/html/notebook/static/css/notebook.css
  15. +19 15 IPython/frontend/html/notebook/static/js/notebook.js
  16. +8 3 IPython/frontend/html/notebook/templates/notebook.html
19 IPython/frontend/html/notebook/static/codemirror2/LICENSE
... ... @@ -0,0 +1,19 @@
  1 +Copyright (C) 2011 by Marijn Haverbeke <marijnh@gmail.com>
  2 +
  3 +Permission is hereby granted, free of charge, to any person obtaining a copy
  4 +of this software and associated documentation files (the "Software"), to deal
  5 +in the Software without restriction, including without limitation the rights
  6 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7 +copies of the Software, and to permit persons to whom the Software is
  8 +furnished to do so, subject to the following conditions:
  9 +
  10 +The above copyright notice and this permission notice shall be included in
  11 +all copies or substantial portions of the Software.
  12 +
  13 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  19 +THE SOFTWARE.
58 IPython/frontend/html/notebook/static/codemirror2/lib/codemirror.css
... ... @@ -0,0 +1,58 @@
  1 +.CodeMirror {
  2 + overflow: hidden; /* Changed from auto to remove scrollbar */
  3 + height: auto; /* Changed to auto to autogrow */
  4 +/* line-height: 1em;*/
  5 + font-family: monospace;
  6 +}
  7 +
  8 +.CodeMirror-gutter {
  9 + position: absolute; left: 0; top: 0;
  10 + background-color: #f7f7f7;
  11 + border-right: 1px solid #eee;
  12 + min-width: 2em;
  13 + height: 100%;
  14 +}
  15 +.CodeMirror-gutter-text {
  16 + color: #aaa;
  17 + text-align: right;
  18 + padding: .4em .2em .4em .4em;
  19 +}
  20 +.CodeMirror-lines {
  21 + /* Changed from 0.4em, but this gives us jitters upon typing, but I found
  22 + that removing .CodeMirror line-height: 1em above, it goes away. */
  23 + padding: 0em;
  24 +}
  25 +
  26 +.CodeMirror pre {
  27 + -moz-border-radius: 0;
  28 + -webkit-border-radius: 0;
  29 + -o-border-radius: 0;
  30 + border-radius: 0;
  31 + border-width: 0; margin: 0; padding: 0; background: transparent;
  32 + font-family: inherit;
  33 +}
  34 +
  35 +.CodeMirror textarea {
  36 + font-family: monospace !important;
  37 +}
  38 +
  39 +.CodeMirror-cursor {
  40 + z-index: 10;
  41 + position: absolute;
  42 + visibility: hidden;
  43 + border-left: 1px solid black !important;
  44 +}
  45 +.CodeMirror-focused .CodeMirror-cursor {
  46 + visibility: visible;
  47 +}
  48 +
  49 +span.CodeMirror-selected {
  50 + background: #ccc !important;
  51 + color: HighlightText !important;
  52 +}
  53 +.CodeMirror-focused span.CodeMirror-selected {
  54 + background: Highlight !important;
  55 +}
  56 +
  57 +.CodeMirror-matchingbracket {color: #0f0 !important;}
  58 +.CodeMirror-nonmatchingbracket {color: #f22 !important;}
1,985 IPython/frontend/html/notebook/static/codemirror2/lib/codemirror.js
... ... @@ -0,0 +1,1985 @@
  1 +// All functions that need access to the editor's state live inside
  2 +// the CodeMirror function. Below that, at the bottom of the file,
  3 +// some utilities are defined.
  4 +
  5 +// CodeMirror is the only global var we claim
  6 +var CodeMirror = (function() {
  7 + // This is the function that produces an editor instance. It's
  8 + // closure is used to store the editor state.
  9 + function CodeMirror(place, givenOptions) {
  10 + // Determine effective options based on given values and defaults.
  11 + var options = {}, defaults = CodeMirror.defaults;
  12 + for (var opt in defaults)
  13 + if (defaults.hasOwnProperty(opt))
  14 + options[opt] = (givenOptions && givenOptions.hasOwnProperty(opt) ? givenOptions : defaults)[opt];
  15 +
  16 + var targetDocument = options["document"];
  17 + // The element in which the editor lives. Takes care of scrolling
  18 + // (if enabled).
  19 + var wrapper = targetDocument.createElement("div");
  20 + wrapper.className = "CodeMirror";
  21 + // Work around http://www.quirksmode.org/bugreports/archives/2006/09/Overflow_Hidden_not_hiding.html
  22 + if (window.ActiveXObject && /MSIE [1-7]\b/.test(navigator.userAgent))
  23 + wrapper.style.position = "relative";
  24 + // This mess creates the base DOM structure for the editor.
  25 + wrapper.innerHTML =
  26 + '<div style="position: relative">' + // Set to the height of the text, causes scrolling
  27 + '<div style="position: absolute; height: 0; width: 0; overflow: hidden;"></div>' +
  28 + '<div style="position: relative">' + // Moved around its parent to cover visible view
  29 + '<div class="CodeMirror-gutter"><div class="CodeMirror-gutter-text"></div></div>' +
  30 + '<div style="overflow: hidden; position: absolute; width: 1px; height: 0; left: 0">' + // Wraps and hides input textarea
  31 + '<textarea style="position: absolute; width: 100000px;" wrap="off"></textarea></div>' +
  32 + // Provides positioning relative to (visible) text origin
  33 + '<div class="CodeMirror-lines"><div style="position: relative">' +
  34 + '<pre class="CodeMirror-cursor">&#160;</pre>' + // Absolutely positioned blinky cursor
  35 + '<div></div></div></div></div></div>'; // This DIV contains the actual code
  36 + if (place.appendChild) place.appendChild(wrapper); else place(wrapper);
  37 + // I've never seen more elegant code in my life.
  38 + var code = wrapper.firstChild, measure = code.firstChild, mover = measure.nextSibling,
  39 + gutter = mover.firstChild, gutterText = gutter.firstChild,
  40 + inputDiv = gutter.nextSibling, input = inputDiv.firstChild,
  41 + lineSpace = inputDiv.nextSibling.firstChild, cursor = lineSpace.firstChild, lineDiv = cursor.nextSibling;
  42 + if (options.tabindex != null) input.tabindex = options.tabindex;
  43 + if (!options.gutter && !options.lineNumbers) gutter.style.display = "none";
  44 +
  45 + // Delayed object wrap timeouts, making sure only one is active. blinker holds an interval.
  46 + var poll = new Delayed(), highlight = new Delayed(), blinker;
  47 +
  48 + // mode holds a mode API object. lines an array of Line objects
  49 + // (see Line constructor), work an array of lines that should be
  50 + // parsed, and history the undo history (instance of History
  51 + // constructor).
  52 + var mode, lines = [new Line("")], work, history = new History(), focused;
  53 + loadMode();
  54 + // The selection. These are always maintained to point at valid
  55 + // positions. Inverted is used to remember that the user is
  56 + // selecting bottom-to-top.
  57 + var sel = {from: {line: 0, ch: 0}, to: {line: 0, ch: 0}, inverted: false};
  58 + // Selection-related flags. shiftSelecting obviously tracks
  59 + // whether the user is holding shift. reducedSelection is a hack
  60 + // to get around the fact that we can't create inverted
  61 + // selections. See below.
  62 + var shiftSelecting, reducedSelection, lastDoubleClick;
  63 + // Variables used by startOperation/endOperation to track what
  64 + // happened during the operation.
  65 + var updateInput, changes, textChanged, selectionChanged, leaveInputAlone;
  66 + // Current visible range (may be bigger than the view window).
  67 + var showingFrom = 0, showingTo = 0, lastHeight = 0, curKeyId = null;
  68 + // editing will hold an object describing the things we put in the
  69 + // textarea, to help figure out whether something changed.
  70 + // bracketHighlighted is used to remember that a backet has been
  71 + // marked.
  72 + var editing, bracketHighlighted;
  73 + // Tracks the maximum line length so that the horizontal scrollbar
  74 + // can be kept static when scrolling.
  75 + var maxLine = "";
  76 +
  77 + // Initialize the content. Somewhat hacky (delayed prepareInput)
  78 + // to work around browser issues.
  79 + operation(function(){setValue(options.value || ""); updateInput = false;})();
  80 + setTimeout(prepareInput, 20);
  81 +
  82 + // Register our event handlers.
  83 + connect(wrapper, "mousedown", operation(onMouseDown));
  84 + // Gecko browsers fire contextmenu *after* opening the menu, at
  85 + // which point we can't mess with it anymore. Context menu is
  86 + // handled in onMouseDown for Gecko.
  87 + if (!gecko) connect(wrapper, "contextmenu", operation(onContextMenu));
  88 + connect(code, "dblclick", operation(onDblClick));
  89 + connect(wrapper, "scroll", function() {updateDisplay([]); if (options.onScroll) options.onScroll(instance);});
  90 + connect(window, "resize", function() {updateDisplay(true);});
  91 + connect(input, "keyup", operation(onKeyUp));
  92 + connect(input, "keydown", operation(onKeyDown));
  93 + connect(input, "keypress", operation(onKeyPress));
  94 + connect(input, "focus", onFocus);
  95 + connect(input, "blur", onBlur);
  96 +
  97 + connect(wrapper, "dragenter", function(e){e.stop();});
  98 + connect(wrapper, "dragover", function(e){e.stop();});
  99 + connect(wrapper, "drop", operation(onDrop));
  100 + connect(wrapper, "paste", function(){input.focus(); fastPoll();});
  101 + connect(input, "paste", function(){fastPoll();});
  102 + connect(input, "cut", function(){fastPoll();});
  103 +
  104 + if (targetDocument.activeElement == input) onFocus();
  105 + else onBlur();
  106 +
  107 + function isLine(l) {return l >= 0 && l < lines.length;}
  108 + // The instance object that we'll return. Mostly calls out to
  109 + // local functions in the CodeMirror function. Some do some extra
  110 + // range checking and/or clipping. operation is used to wrap the
  111 + // call so that changes it makes are tracked, and the display is
  112 + // updated afterwards.
  113 + var instance = {
  114 + getValue: getValue,
  115 + setValue: operation(setValue),
  116 + getSelection: getSelection,
  117 + replaceSelection: operation(replaceSelection),
  118 + focus: function(){input.focus(); onFocus(); fastPoll();},
  119 + setOption: function(option, value) {
  120 + options[option] = value;
  121 + if (option == "lineNumbers" || option == "gutter") gutterChanged();
  122 + else if (option == "mode" || option == "indentUnit") loadMode();
  123 + },
  124 + getOption: function(option) {return options[option];},
  125 + undo: operation(undo),
  126 + redo: operation(redo),
  127 + indentLine: operation(function(n) {if (isLine(n)) indentLine(n, "smart");}),
  128 + historySize: function() {return {undo: history.done.length, redo: history.undone.length};},
  129 + matchBrackets: operation(function(){matchBrackets(true);}),
  130 + getTokenAt: function(pos) {
  131 + pos = clipPos(pos);
  132 + return lines[pos.line].getTokenAt(mode, getStateBefore(pos.line), pos.ch);
  133 + },
  134 + cursorCoords: function(start){
  135 + if (start == null) start = sel.inverted;
  136 + return pageCoords(start ? sel.from : sel.to);
  137 + },
  138 + charCoords: function(pos){return pageCoords(clipPos(pos));},
  139 + coordsChar: function(coords) {
  140 + var off = eltOffset(lineSpace);
  141 + var line = clipLine(Math.min(lines.length - 1, showingFrom + Math.floor((coords.y - off.top) / lineHeight())));
  142 + return clipPos({line: line, ch: charFromX(clipLine(line), coords.x - off.left)});
  143 + },
  144 + getSearchCursor: function(query, pos, caseFold) {return new SearchCursor(query, pos, caseFold);},
  145 + markText: operation(function(a, b, c){return operation(markText(a, b, c));}),
  146 + setMarker: addGutterMarker,
  147 + clearMarker: removeGutterMarker,
  148 + setLineClass: operation(setLineClass),
  149 + lineInfo: lineInfo,
  150 + addWidget: function(pos, node, scroll) {
  151 + var pos = localCoords(clipPos(pos), true);
  152 + node.style.top = (showingFrom * lineHeight() + pos.yBot + paddingTop()) + "px";
  153 + node.style.left = (pos.x + paddingLeft()) + "px";
  154 + code.appendChild(node);
  155 + if (scroll)
  156 + scrollIntoView(pos.x, pos.yBot, pos.x + node.offsetWidth, pos.yBot + node.offsetHeight);
  157 + },
  158 +
  159 + lineCount: function() {return lines.length;},
  160 + getCursor: function(start) {
  161 + if (start == null) start = sel.inverted;
  162 + return copyPos(start ? sel.from : sel.to);
  163 + },
  164 + somethingSelected: function() {return !posEq(sel.from, sel.to);},
  165 + setCursor: operation(function(line, ch) {
  166 + if (ch == null && typeof line.line == "number") setCursor(line.line, line.ch);
  167 + else setCursor(line, ch);
  168 + }),
  169 + setSelection: operation(function(from, to) {setSelection(clipPos(from), clipPos(to || from));}),
  170 + getLine: function(line) {if (isLine(line)) return lines[line].text;},
  171 + setLine: operation(function(line, text) {
  172 + if (isLine(line)) replaceRange(text, {line: line, ch: 0}, {line: line, ch: lines[line].text.length});
  173 + }),
  174 + removeLine: operation(function(line) {
  175 + if (isLine(line)) replaceRange("", {line: line, ch: 0}, clipPos({line: line+1, ch: 0}));
  176 + }),
  177 + replaceRange: operation(replaceRange),
  178 + getRange: function(from, to) {return getRange(clipPos(from), clipPos(to));},
  179 +
  180 + operation: function(f){return operation(f)();},
  181 + refresh: function(){updateDisplay(true);},
  182 + getInputField: function(){return input;},
  183 + getWrapperElement: function(){return wrapper;}
  184 + };
  185 +
  186 + function setValue(code) {
  187 + history = null;
  188 + var top = {line: 0, ch: 0};
  189 + updateLines(top, {line: lines.length - 1, ch: lines[lines.length-1].text.length},
  190 + splitLines(code), top, top);
  191 + history = new History();
  192 + }
  193 + function getValue(code) {
  194 + var text = [];
  195 + for (var i = 0, l = lines.length; i < l; ++i)
  196 + text.push(lines[i].text);
  197 + return text.join("\n");
  198 + }
  199 +
  200 + function onMouseDown(e) {
  201 + var ld = lastDoubleClick; lastDoubleClick = null;
  202 + // First, see if this is a click in the gutter
  203 + for (var n = e.target(); n != wrapper; n = n.parentNode)
  204 + if (n.parentNode == gutterText) {
  205 + if (options.onGutterClick)
  206 + options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom);
  207 + return e.stop();
  208 + }
  209 +
  210 + if (gecko && e.button() == 3) onContextMenu(e);
  211 + if (e.button() != 1) return;
  212 + // For button 1, if it was clicked inside the editor
  213 + // (posFromMouse returning non-null), we have to adjust the
  214 + // selection.
  215 + var start = posFromMouse(e), last = start, going;
  216 + if (!start) {if (e.target() == wrapper) e.stop(); return;}
  217 +
  218 + if (!focused) onFocus();
  219 + e.stop();
  220 + if (ld && +new Date - ld < 400) return selectLine(start.line);
  221 +
  222 + setCursor(start.line, start.ch, true);
  223 + // And then we have to see if it's a drag event, in which case
  224 + // the dragged-over text must be selected.
  225 + function end() {
  226 + input.focus();
  227 + updateInput = true;
  228 + move(); up();
  229 + }
  230 + function extend(e) {
  231 + var cur = posFromMouse(e, true);
  232 + if (cur && !posEq(cur, last)) {
  233 + if (!focused) onFocus();
  234 + last = cur;
  235 + setSelectionUser(start, cur);
  236 + updateInput = false;
  237 + var visible = visibleLines();
  238 + if (cur.line >= visible.to || cur.line < visible.from)
  239 + going = setTimeout(operation(function(){extend(e);}), 150);
  240 + }
  241 + }
  242 +
  243 + var move = connect(targetDocument, "mousemove", operation(function(e) {
  244 + clearTimeout(going);
  245 + e.stop();
  246 + extend(e);
  247 + }), true);
  248 + var up = connect(targetDocument, "mouseup", operation(function(e) {
  249 + clearTimeout(going);
  250 + var cur = posFromMouse(e);
  251 + if (cur) setSelectionUser(start, cur);
  252 + e.stop();
  253 + end();
  254 + }), true);
  255 + }
  256 + function onDblClick(e) {
  257 + var pos = posFromMouse(e);
  258 + if (!pos) return;
  259 + selectWordAt(pos);
  260 + e.stop();
  261 + lastDoubleClick = +new Date;
  262 + }
  263 + function onDrop(e) {
  264 + var pos = posFromMouse(e, true), files = e.e.dataTransfer.files;
  265 + if (!pos || options.readOnly) return;
  266 + if (files && files.length && window.FileReader && window.File) {
  267 + var n = files.length, text = Array(n), read = 0;
  268 + for (var i = 0; i < n; ++i) loadFile(files[i], i);
  269 + function loadFile(file, i) {
  270 + var reader = new FileReader;
  271 + reader.onload = function() {
  272 + text[i] = reader.result;
  273 + if (++read == n) replaceRange(text.join(""), clipPos(pos), clipPos(pos));
  274 + };
  275 + reader.readAsText(file);
  276 + }
  277 + }
  278 + else {
  279 + try {
  280 + var text = e.e.dataTransfer.getData("Text");
  281 + if (text) replaceRange(text, pos, pos);
  282 + }
  283 + catch(e){}
  284 + }
  285 + }
  286 + function onKeyDown(e) {
  287 + if (!focused) onFocus();
  288 +
  289 + var code = e.e.keyCode;
  290 + // Tries to detect ctrl on non-mac, cmd on mac.
  291 + var mod = (mac ? e.e.metaKey : e.e.ctrlKey) && !e.e.altKey, anyMod = e.e.ctrlKey || e.e.altKey || e.e.metaKey;
  292 + if (code == 16 || e.e.shiftKey) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from);
  293 + else shiftSelecting = null;
  294 + // First give onKeyEvent option a chance to handle this.
  295 + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e.e))) return;
  296 +
  297 + if (code == 33 || code == 34) {scrollPage(code == 34); return e.stop();} // page up/down
  298 + if (mod && ((code == 36 || code == 35) || // ctrl-home/end
  299 + mac && (code == 38 || code == 40))) { // cmd-up/down
  300 + scrollEnd(code == 36 || code == 38); return e.stop();
  301 + }
  302 + if (mod && code == 65) {selectAll(); return e.stop();} // ctrl-a
  303 + if (!options.readOnly) {
  304 + if (!anyMod && code == 13) {return;} // enter
  305 + if (!anyMod && code == 9 && handleTab(e.e.shiftKey)) return e.stop(); // tab
  306 + if (mod && code == 90) {undo(); return e.stop();} // ctrl-z
  307 + if (mod && ((e.e.shiftKey && code == 90) || code == 89)) {redo(); return e.stop();} // ctrl-shift-z, ctrl-y
  308 + }
  309 +
  310 + // Key id to use in the movementKeys map. We also pass it to
  311 + // fastPoll in order to 'self learn'. We need this because
  312 + // reducedSelection, the hack where we collapse the selection to
  313 + // its start when it is inverted and a movement key is pressed
  314 + // (and later restore it again), shouldn't be used for
  315 + // non-movement keys.
  316 + curKeyId = (mod ? "c" : "") + code;
  317 + if (sel.inverted && movementKeys.hasOwnProperty(curKeyId)) {
  318 + var range = selRange(input);
  319 + if (range) {
  320 + reducedSelection = {anchor: range.start};
  321 + setSelRange(input, range.start, range.start);
  322 + }
  323 + }
  324 + fastPoll(curKeyId);
  325 + }
  326 + function onKeyUp(e) {
  327 + if (reducedSelection) {
  328 + reducedSelection = null;
  329 + updateInput = true;
  330 + }
  331 + if (e.e.keyCode == 16) shiftSelecting = null;
  332 + }
  333 + function onKeyPress(e) {
  334 + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e.e))) return;
  335 + if (options.electricChars && mode.electricChars) {
  336 + var ch = String.fromCharCode(e.e.charCode == null ? e.e.keyCode : e.e.charCode);
  337 + if (mode.electricChars.indexOf(ch) > -1)
  338 + setTimeout(operation(function() {indentLine(sel.to.line, "smart");}), 50);
  339 + }
  340 + var code = e.e.keyCode;
  341 + // Re-stop tab and enter. Necessary on some browsers.
  342 + if (code == 13) {if (!options.readOnly) handleEnter(); e.stop();}
  343 + else if (!e.e.ctrlKey && !e.e.altKey && !e.e.metaKey && code == 9 && options.tabMode != "default") e.stop();
  344 + else fastPoll(curKeyId);
  345 + }
  346 +
  347 + function onFocus() {
  348 + if (!focused && options.onFocus) options.onFocus(instance);
  349 + focused = true;
  350 + slowPoll();
  351 + if (wrapper.className.search(/\bCodeMirror-focused\b/) == -1)
  352 + wrapper.className += " CodeMirror-focused";
  353 + restartBlink();
  354 + }
  355 + function onBlur() {
  356 + if (focused && options.onBlur) options.onBlur(instance);
  357 + clearInterval(blinker);
  358 + shiftSelecting = null;
  359 + focused = false;
  360 + wrapper.className = wrapper.className.replace(" CodeMirror-focused", "");
  361 + }
  362 +
  363 + // Replace the range from from to to by the strings in newText.
  364 + // Afterwards, set the selection to selFrom, selTo.
  365 + function updateLines(from, to, newText, selFrom, selTo) {
  366 + if (history) {
  367 + var old = [];
  368 + for (var i = from.line, e = to.line + 1; i < e; ++i) old.push(lines[i].text);
  369 + history.addChange(from.line, newText.length, old);
  370 + while (history.done.length > options.undoDepth) history.done.shift();
  371 + }
  372 + updateLinesNoUndo(from, to, newText, selFrom, selTo);
  373 + }
  374 + function unredoHelper(from, to) {
  375 + var change = from.pop();
  376 + if (change) {
  377 + var replaced = [], end = change.start + change.added;
  378 + for (var i = change.start; i < end; ++i) replaced.push(lines[i].text);
  379 + to.push({start: change.start, added: change.old.length, old: replaced});
  380 + var pos = clipPos({line: change.start + change.old.length - 1,
  381 + ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])});
  382 + updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: lines[end-1].text.length}, change.old, pos, pos);
  383 + }
  384 + }
  385 + function undo() {unredoHelper(history.done, history.undone);}
  386 + function redo() {unredoHelper(history.undone, history.done);}
  387 +
  388 + function updateLinesNoUndo(from, to, newText, selFrom, selTo) {
  389 + var recomputeMaxLength = false, maxLineLength = maxLine.length;
  390 + for (var i = from.line; i < to.line; ++i) {
  391 + if (lines[i].text.length == maxLineLength) {recomputeMaxLength = true; break;}
  392 + }
  393 +
  394 + var nlines = to.line - from.line, firstLine = lines[from.line], lastLine = lines[to.line];
  395 + // First adjust the line structure, taking some care to leave highlighting intact.
  396 + if (firstLine == lastLine) {
  397 + if (newText.length == 1)
  398 + firstLine.replace(from.ch, to.ch, newText[0]);
  399 + else {
  400 + lastLine = firstLine.split(to.ch, newText[newText.length-1]);
  401 + var spliceargs = [from.line + 1, nlines];
  402 + firstLine.replace(from.ch, firstLine.text.length, newText[0]);
  403 + for (var i = 1, e = newText.length - 1; i < e; ++i) spliceargs.push(new Line(newText[i]));
  404 + spliceargs.push(lastLine);
  405 + lines.splice.apply(lines, spliceargs);
  406 + }
  407 + }
  408 + else if (newText.length == 1) {
  409 + firstLine.replace(from.ch, firstLine.text.length, newText[0] + lastLine.text.slice(to.ch));
  410 + lines.splice(from.line + 1, nlines);
  411 + }
  412 + else {
  413 + var spliceargs = [from.line + 1, nlines - 1];
  414 + firstLine.replace(from.ch, firstLine.text.length, newText[0]);
  415 + lastLine.replace(0, to.ch, newText[newText.length-1]);
  416 + for (var i = 1, e = newText.length - 1; i < e; ++i) spliceargs.push(new Line(newText[i]));
  417 + lines.splice.apply(lines, spliceargs);
  418 + }
  419 +
  420 +
  421 + for (var i = from.line, e = i + newText.length; i < e; ++i) {
  422 + var l = lines[i].text;
  423 + if (l.length > maxLineLength) {
  424 + maxLine = l; maxLineLength = l.length;
  425 + recomputeMaxLength = false;
  426 + }
  427 + }
  428 + if (recomputeMaxLength) {
  429 + maxLineLength = 0;
  430 + for (var i = 0, e = lines.length; i < e; ++i) {
  431 + var l = lines[i].text;
  432 + if (l.length > maxLineLength) {
  433 + maxLineLength = l.length; maxLine = l;
  434 + }
  435 + }
  436 + }
  437 +
  438 + // Add these lines to the work array, so that they will be
  439 + // highlighted. Adjust work lines if lines were added/removed.
  440 + var newWork = [], lendiff = newText.length - nlines - 1;
  441 + for (var i = 0, l = work.length; i < l; ++i) {
  442 + var task = work[i];
  443 + if (task < from.line) newWork.push(task);
  444 + else if (task > to.line) newWork.push(task + lendiff);
  445 + }
  446 + if (newText.length) newWork.push(from.line);
  447 + work = newWork;
  448 + startWorker(100);
  449 + // Remember that these lines changed, for updating the display
  450 + changes.push({from: from.line, to: to.line + 1, diff: lendiff});
  451 + textChanged = {from: from, to: to, text: newText};
  452 +
  453 + // Update the selection
  454 + function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;}
  455 + setSelection(selFrom, selTo, updateLine(sel.from.line), updateLine(sel.to.line));
  456 +
  457 + // Make sure the scroll-size div has the correct height.
  458 + code.style.height = (lines.length * lineHeight() + 2 * paddingTop()) + "px";
  459 + }
  460 +
  461 + function replaceRange(code, from, to) {
  462 + from = clipPos(from);
  463 + if (!to) to = from; else to = clipPos(to);
  464 + code = splitLines(code);
  465 + function adjustPos(pos) {
  466 + if (posLess(pos, from)) return pos;
  467 + if (!posLess(to, pos)) return end;
  468 + var line = pos.line + code.length - (to.line - from.line) - 1;
  469 + var ch = pos.ch;
  470 + if (pos.line == to.line)
  471 + ch += code[code.length-1].length - (to.ch - (to.line == from.line ? from.ch : 0));
  472 + return {line: line, ch: ch};
  473 + }
  474 + var end;
  475 + replaceRange1(code, from, to, function(end1) {
  476 + end = end1;
  477 + return {from: adjustPos(sel.from), to: adjustPos(sel.to)};
  478 + });
  479 + return end;
  480 + }
  481 + function replaceSelection(code, collapse) {
  482 + replaceRange1(splitLines(code), sel.from, sel.to, function(end) {
  483 + if (collapse == "end") return {from: end, to: end};
  484 + else if (collapse == "start") return {from: sel.from, to: sel.from};
  485 + else return {from: sel.from, to: end};
  486 + });
  487 + }
  488 + function replaceRange1(code, from, to, computeSel) {
  489 + var endch = code.length == 1 ? code[0].length + from.ch : code[code.length-1].length;
  490 + var newSel = computeSel({line: from.line + code.length - 1, ch: endch});
  491 + updateLines(from, to, code, newSel.from, newSel.to);
  492 + }
  493 +
  494 + function getRange(from, to) {
  495 + var l1 = from.line, l2 = to.line;
  496 + if (l1 == l2) return lines[l1].text.slice(from.ch, to.ch);
  497 + var code = [lines[l1].text.slice(from.ch)];
  498 + for (var i = l1 + 1; i < l2; ++i) code.push(lines[i].text);
  499 + code.push(lines[l2].text.slice(0, to.ch));
  500 + return code.join("\n");
  501 + }
  502 + function getSelection() {
  503 + return getRange(sel.from, sel.to);
  504 + }
  505 +
  506 + var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll
  507 + function slowPoll() {
  508 + if (pollingFast) return;
  509 + poll.set(2000, function() {
  510 + startOperation();
  511 + readInput();
  512 + if (focused) slowPoll();
  513 + endOperation();
  514 + });
  515 + }
  516 + function fastPoll(keyId) {
  517 + var missed = false;
  518 + pollingFast = true;
  519 + function p() {
  520 + startOperation();
  521 + var changed = readInput();
  522 + if (changed == "moved" && keyId) movementKeys[keyId] = true;
  523 + if (!changed && !missed) {missed = true; poll.set(80, p);}
  524 + else {pollingFast = false; slowPoll();}
  525 + endOperation();
  526 + }
  527 + poll.set(20, p);
  528 + }
  529 +
  530 + // Inspects the textarea, compares its state (content, selection)
  531 + // to the data in the editing variable, and updates the editor
  532 + // content or cursor if something changed.
  533 + function readInput() {
  534 + var changed = false, text = input.value, sr = selRange(input);
  535 + if (!sr) return false;
  536 + var changed = editing.text != text, rs = reducedSelection;
  537 + var moved = changed || sr.start != editing.start || sr.end != (rs ? editing.start : editing.end);
  538 + if (!moved && !rs) return false;
  539 + if (changed) {
  540 + shiftSelecting = reducedSelection = null;
  541 + if (options.readOnly) {updateInput = true; return "changed";}
  542 + }
  543 +
  544 + // Compute selection start and end based on start/end offsets in textarea
  545 + function computeOffset(n, startLine) {
  546 + var pos = 0;
  547 + for (;;) {
  548 + var found = text.indexOf("\n", pos);
  549 + if (found == -1 || (text.charAt(found-1) == "\r" ? found - 1 : found) >= n)
  550 + return {line: startLine, ch: n - pos};
  551 + ++startLine;
  552 + pos = found + 1;
  553 + }
  554 + }
  555 + var from = computeOffset(sr.start, editing.from),
  556 + to = computeOffset(sr.end, editing.from);
  557 + // Here we have to take the reducedSelection hack into account,
  558 + // so that you can, for example, press shift-up at the start of
  559 + // your selection and have the right thing happen.
  560 + if (rs) {
  561 + from = sr.start == rs.anchor ? to : from;
  562 + to = shiftSelecting ? sel.to : sr.start == rs.anchor ? from : to;
  563 + if (!posLess(from, to)) {
  564 + reducedSelection = null;
  565 + sel.inverted = false;
  566 + var tmp = from; from = to; to = tmp;
  567 + }
  568 + }
  569 +
  570 + // In some cases (cursor on same line as before), we don't have
  571 + // to update the textarea content at all.
  572 + if (from.line == to.line && from.line == sel.from.line && from.line == sel.to.line && !shiftSelecting)
  573 + updateInput = false;
  574 +
  575 + // Magic mess to extract precise edited range from the changed
  576 + // string.
  577 + if (changed) {
  578 + var start = 0, end = text.length, len = Math.min(end, editing.text.length);
  579 + var c, line = editing.from, nl = -1;
  580 + while (start < len && (c = text.charAt(start)) == editing.text.charAt(start)) {
  581 + ++start;
  582 + if (c == "\n") {line++; nl = start;}
  583 + }
  584 + var ch = nl > -1 ? start - nl : start, endline = editing.to - 1, edend = editing.text.length;
  585 + for (;;) {
  586 + c = editing.text.charAt(edend);
  587 + if (c == "\n") endline--;
  588 + if (text.charAt(end) != c) {++end; ++edend; break;}
  589 + if (edend <= start || end <= start) break;
  590 + --end; --edend;
  591 + }
  592 + var nl = editing.text.lastIndexOf("\n", edend - 1), endch = nl == -1 ? edend : edend - nl - 1;
  593 + updateLines({line: line, ch: ch}, {line: endline, ch: endch}, splitLines(text.slice(start, end)), from, to);
  594 + if (line != endline || from.line != line) updateInput = true;
  595 + }
  596 + else setSelection(from, to);
  597 +
  598 + editing.text = text; editing.start = sr.start; editing.end = sr.end;
  599 + return changed ? "changed" : moved ? "moved" : false;
  600 + }
  601 +
  602 + // Set the textarea content and selection range to match the
  603 + // editor state.
  604 + function prepareInput() {
  605 + var text = [];
  606 + var from = Math.max(0, sel.from.line - 1), to = Math.min(lines.length, sel.to.line + 2);
  607 + for (var i = from; i < to; ++i) text.push(lines[i].text);
  608 + text = input.value = text.join(lineSep);
  609 + var startch = sel.from.ch, endch = sel.to.ch;
  610 + for (var i = from; i < sel.from.line; ++i)
  611 + startch += lineSep.length + lines[i].text.length;
  612 + for (var i = from; i < sel.to.line; ++i)
  613 + endch += lineSep.length + lines[i].text.length;
  614 + editing = {text: text, from: from, to: to, start: startch, end: endch};
  615 + setSelRange(input, startch, reducedSelection ? startch : endch);
  616 + }
  617 +
  618 + function scrollCursorIntoView() {
  619 + var cursor = localCoords(sel.inverted ? sel.from : sel.to);
  620 + return scrollIntoView(cursor.x, cursor.y, cursor.x, cursor.yBot);
  621 + }
  622 + function scrollIntoView(x1, y1, x2, y2) {
  623 + var pl = paddingLeft(), pt = paddingTop(), lh = lineHeight();
  624 + y1 += pt; y2 += pt; x1 += pl; x2 += pl;
  625 + var screen = wrapper.clientHeight, screentop = wrapper.scrollTop, scrolled = false, result = true;
  626 + if (y1 < screentop) {wrapper.scrollTop = Math.max(0, y1 - 2*lh); scrolled = true;}
  627 + else if (y2 > screentop + screen) {wrapper.scrollTop = y2 + lh - screen; scrolled = true;}
  628 +
  629 + var screenw = wrapper.clientWidth, screenleft = wrapper.scrollLeft;
  630 + if (x1 < screenleft) {wrapper.scrollLeft = Math.max(0, x1 - 10); scrolled = true;}
  631 + else if (x2 > screenw + screenleft) {
  632 + wrapper.scrollLeft = x2 + 10 - screenw;
  633 + scrolled = true;
  634 + if (x2 > code.clientWidth) result = false;
  635 + }
  636 + if (scrolled && options.onScroll) options.onScroll(instance);
  637 + return result;
  638 + }
  639 +
  640 + function visibleLines() {
  641 + var lh = lineHeight(), top = wrapper.scrollTop - paddingTop();
  642 + return {from: Math.min(lines.length, Math.max(0, Math.floor(top / lh))),
  643 + to: Math.min(lines.length, Math.ceil((top + wrapper.clientHeight) / lh))};
  644 + }
  645 + // Uses a set of changes plus the current scroll position to
  646 + // determine which DOM updates have to be made, and makes the
  647 + // updates.
  648 + function updateDisplay(changes) {
  649 + if (!wrapper.clientWidth) {
  650 + showingFrom = showingTo = 0;
  651 + return;
  652 + }
  653 + // First create a range of theoretically intact lines, and punch
  654 + // holes in that using the change info.
  655 + var intact = changes === true ? [] : [{from: showingFrom, to: showingTo, domStart: 0}];
  656 + for (var i = 0, l = changes.length || 0; i < l; ++i) {
  657 + var change = changes[i], intact2 = [], diff = change.diff || 0;
  658 + for (var j = 0, l2 = intact.length; j < l2; ++j) {
  659 + var range = intact[j];
  660 + if (change.to <= range.from)
  661 + intact2.push({from: range.from + diff, to: range.to + diff, domStart: range.domStart});
  662 + else if (range.to <= change.from)
  663 + intact2.push(range);
  664 + else {
  665 + if (change.from > range.from)
  666 + intact2.push({from: range.from, to: change.from, domStart: range.domStart})
  667 + if (change.to < range.to)
  668 + intact2.push({from: change.to + diff, to: range.to + diff,
  669 + domStart: range.domStart + (change.to - range.from)});
  670 + }
  671 + }
  672 + intact = intact2;
  673 + }
  674 +
  675 + // Then, determine which lines we'd want to see, and which
  676 + // updates have to be made to get there.
  677 + var visible = visibleLines();
  678 + var from = Math.min(showingFrom, Math.max(visible.from - 3, 0)),
  679 + to = Math.min(lines.length, Math.max(showingTo, visible.to + 3)),
  680 + updates = [], domPos = 0, domEnd = showingTo - showingFrom, pos = from, changedLines = 0;
  681 +
  682 + for (var i = 0, l = intact.length; i < l; ++i) {
  683 + var range = intact[i];
  684 + if (range.to <= from) continue;
  685 + if (range.from >= to) break;
  686 + if (range.domStart > domPos || range.from > pos) {
  687 + updates.push({from: pos, to: range.from, domSize: range.domStart - domPos, domStart: domPos});
  688 + changedLines += range.from - pos;
  689 + }
  690 + pos = range.to;
  691 + domPos = range.domStart + (range.to - range.from);
  692 + }
  693 + if (domPos != domEnd || pos != to) {
  694 + changedLines += Math.abs(to - pos);
  695 + updates.push({from: pos, to: to, domSize: domEnd - domPos, domStart: domPos});
  696 + }
  697 +
  698 + if (!updates.length) return;
  699 + lineDiv.style.display = "none";
  700 + // If more than 30% of the screen needs update, just do a full
  701 + // redraw (which is quicker than patching)
  702 + if (changedLines > (visible.to - visible.from) * .3)
  703 + refreshDisplay(from = Math.max(visible.from - 10, 0), to = Math.min(visible.to + 7, lines.length));
  704 + // Otherwise, only update the stuff that needs updating.
  705 + else
  706 + patchDisplay(updates);
  707 + lineDiv.style.display = "";
  708 +
  709 + // Position the mover div to align with the lines it's supposed
  710 + // to be showing (which will cover the visible display)
  711 + var different = from != showingFrom || to != showingTo || lastHeight != wrapper.clientHeight;
  712 + showingFrom = from; showingTo = to;
  713 + mover.style.top = (from * lineHeight()) + "px";
  714 + if (different) {
  715 + lastHeight = wrapper.clientHeight;
  716 + code.style.height = (lines.length * lineHeight() + 2 * paddingTop()) + "px";
  717 + updateGutter();
  718 + }
  719 +
  720 + var textWidth = stringWidth(maxLine);
  721 + lineSpace.style.width = textWidth > wrapper.clientWidth ? textWidth + "px" : "";
  722 +
  723 + // Since this is all rather error prone, it is honoured with the
  724 + // only assertion in the whole file.
  725 + if (lineDiv.childNodes.length != showingTo - showingFrom)
  726 + throw new Error("BAD PATCH! " + JSON.stringify(updates) + " size=" + (showingTo - showingFrom) +
  727 + " nodes=" + lineDiv.childNodes.length);
  728 + updateCursor();
  729 + }
  730 +
  731 + function refreshDisplay(from, to) {
  732 + var html = [], start = {line: from, ch: 0}, inSel = posLess(sel.from, start) && !posLess(sel.to, start);
  733 + for (var i = from; i < to; ++i) {
  734 + var ch1 = null, ch2 = null;
  735 + if (inSel) {
  736 + ch1 = 0;
  737 + if (sel.to.line == i) {inSel = false; ch2 = sel.to.ch;}
  738 + }
  739 + else if (sel.from.line == i) {
  740 + if (sel.to.line == i) {ch1 = sel.from.ch; ch2 = sel.to.ch;}
  741 + else {inSel = true; ch1 = sel.from.ch;}
  742 + }
  743 + html.push(lines[i].getHTML(ch1, ch2, true));
  744 + }
  745 + lineDiv.innerHTML = html.join("");
  746 + }
  747 + function patchDisplay(updates) {
  748 + // Slightly different algorithm for IE (badInnerHTML), since
  749 + // there .innerHTML on PRE nodes is dumb, and discards
  750 + // whitespace.
  751 + var sfrom = sel.from.line, sto = sel.to.line, off = 0,
  752 + scratch = badInnerHTML && targetDocument.createElement("div");
  753 + for (var i = 0, e = updates.length; i < e; ++i) {
  754 + var rec = updates[i];
  755 + var extra = (rec.to - rec.from) - rec.domSize;
  756 + var nodeAfter = lineDiv.childNodes[rec.domStart + rec.domSize + off] || null;
  757 + if (badInnerHTML)
  758 + for (var j = Math.max(-extra, rec.domSize); j > 0; --j)
  759 + lineDiv.removeChild(nodeAfter ? nodeAfter.previousSibling : lineDiv.lastChild);
  760 + else if (extra) {
  761 + for (var j = Math.max(0, extra); j > 0; --j)
  762 + lineDiv.insertBefore(targetDocument.createElement("pre"), nodeAfter);
  763 + for (var j = Math.max(0, -extra); j > 0; --j)
  764 + lineDiv.removeChild(nodeAfter ? nodeAfter.previousSibling : lineDiv.lastChild);
  765 + }
  766 + var node = lineDiv.childNodes[rec.domStart + off], inSel = sfrom < rec.from && sto >= rec.from;
  767 + for (var j = rec.from; j < rec.to; ++j) {
  768 + var ch1 = null, ch2 = null;
  769 + if (inSel) {
  770 + ch1 = 0;
  771 + if (sto == j) {inSel = false; ch2 = sel.to.ch;}
  772 + }
  773 + else if (sfrom == j) {
  774 + if (sto == j) {ch1 = sel.from.ch; ch2 = sel.to.ch;}
  775 + else {inSel = true; ch1 = sel.from.ch;}
  776 + }
  777 + if (badInnerHTML) {
  778 + scratch.innerHTML = lines[j].getHTML(ch1, ch2, true);
  779 + lineDiv.insertBefore(scratch.firstChild, nodeAfter);
  780 + }
  781 + else {
  782 + node.innerHTML = lines[j].getHTML(ch1, ch2, false);
  783 + node.className = lines[j].className || "";
  784 + node = node.nextSibling;
  785 + }
  786 + }
  787 + off += extra;
  788 + }
  789 + }
  790 +
  791 + function updateGutter() {
  792 + if (!options.gutter && !options.lineNumbers) return;
  793 + var hText = mover.offsetHeight, hEditor = wrapper.clientHeight;
  794 + gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px";
  795 + var html = [];
  796 + for (var i = showingFrom; i < showingTo; ++i) {
  797 + var marker = lines[i].gutterMarker;
  798 + var text = options.lineNumbers ? i + options.firstLineNumber : null;
  799 + if (marker && marker.text)
  800 + text = marker.text.replace("%N%", text != null ? text : "");
  801 + else if (text == null)
  802 + text = "\u00a0";
  803 + html.push((marker && marker.style ? '<pre class="' + marker.style + '">' : "<pre>"), text, "</pre>");
  804 + }
  805 + gutter.style.display = "none";
  806 + gutterText.innerHTML = html.join("");
  807 + var minwidth = String(lines.length).length, firstNode = gutterText.firstChild, val = eltText(firstNode), pad = "";
  808 + while (val.length + pad.length < minwidth) pad += "\u00a0";
  809 + if (pad) firstNode.insertBefore(targetDocument.createTextNode(pad), firstNode.firstChild);
  810 + gutter.style.display = "";
  811 + lineSpace.style.marginLeft = gutter.offsetWidth + "px";
  812 + }
  813 + function updateCursor() {
  814 + var head = sel.inverted ? sel.from : sel.to;
  815 + var x = charX(head.line, head.ch) + "px", y = (head.line - showingFrom) * lineHeight() + "px";
  816 + inputDiv.style.top = y;
  817 + if (posEq(sel.from, sel.to)) {
  818 + cursor.style.top = y; cursor.style.left = x;
  819 + cursor.style.display = "";
  820 + }
  821 + else cursor.style.display = "none";
  822 + }
  823 +
  824 + function setSelectionUser(from, to) {
  825 + var sh = shiftSelecting && clipPos(shiftSelecting);
  826 + if (sh) {
  827 + if (posLess(sh, from)) from = sh;
  828 + else if (posLess(to, sh)) to = sh;
  829 + }
  830 + setSelection(from, to);
  831 + }
  832 + // Update the selection. Last two args are only used by
  833 + // updateLines, since they have to be expressed in the line
  834 + // numbers before the update.
  835 + function setSelection(from, to, oldFrom, oldTo) {
  836 + if (posEq(sel.from, from) && posEq(sel.to, to)) return;
  837 + if (posLess(to, from)) {var tmp = to; to = from; from = tmp;}
  838 +
  839 + var startEq = posEq(sel.to, to), endEq = posEq(sel.from, from);
  840 + if (posEq(from, to)) sel.inverted = false;
  841 + else if (startEq && !endEq) sel.inverted = true;
  842 + else if (endEq && !startEq) sel.inverted = false;
  843 +
  844 + // Some ugly logic used to only mark the lines that actually did
  845 + // see a change in selection as changed, rather than the whole
  846 + // selected range.
  847 + if (oldFrom == null) {oldFrom = sel.from.line; oldTo = sel.to.line;}
  848 + if (posEq(from, to)) {
  849 + if (!posEq(sel.from, sel.to))
  850 + changes.push({from: oldFrom, to: oldTo + 1});
  851 + }
  852 + else if (posEq(sel.from, sel.to)) {
  853 + changes.push({from: from.line, to: to.line + 1});
  854 + }
  855 + else {
  856 + if (!posEq(from, sel.from)) {
  857 + if (from.line < oldFrom)
  858 + changes.push({from: from.line, to: Math.min(to.line, oldFrom) + 1});
  859 + else
  860 + changes.push({from: oldFrom, to: Math.min(oldTo, from.line) + 1});
  861 + }
  862 + if (!posEq(to, sel.to)) {
  863 + if (to.line < oldTo)
  864 + changes.push({from: Math.max(oldFrom, from.line), to: oldTo + 1});
  865 + else
  866 + changes.push({from: Math.max(from.line, oldTo), to: to.line + 1});
  867 + }
  868 + }
  869 + sel.from = from; sel.to = to;
  870 + selectionChanged = true;
  871 + }
  872 + function setCursor(line, ch, user) {
  873 + var pos = clipPos({line: line, ch: ch || 0});
  874 + (user ? setSelectionUser : setSelection)(pos, pos);
  875 + }
  876 +
  877 + function clipLine(n) {return Math.max(0, Math.min(n, lines.length-1));}
  878 + function clipPos(pos) {
  879 + if (pos.line < 0) return {line: 0, ch: 0};
  880 + if (pos.line >= lines.length) return {line: lines.length-1, ch: lines[lines.length-1].text.length};
  881 + var ch = pos.ch, linelen = lines[pos.line].text.length;
  882 + if (ch == null || ch > linelen) return {line: pos.line, ch: linelen};
  883 + else if (ch < 0) return {line: pos.line, ch: 0};
  884 + else return pos;
  885 + }
  886 +
  887 + function scrollPage(down) {
  888 + var linesPerPage = Math.floor(wrapper.clientHeight / lineHeight()), head = sel.inverted ? sel.from : sel.to;
  889 + setCursor(head.line + (Math.max(linesPerPage - 1, 1) * (down ? 1 : -1)), head.ch, true);
  890 + }
  891 + function scrollEnd(top) {
  892 + setCursor(top ? 0 : lines.length - 1, true);
  893 + }
  894 + function selectAll() {
  895 + var endLine = lines.length - 1;
  896 + setSelection({line: 0, ch: 0}, {line: endLine, ch: lines[endLine].text.length});
  897 + }
  898 + function selectWordAt(pos) {
  899 + var line = lines[pos.line].text;
  900 + var start = pos.ch, end = pos.ch;
  901 + while (start > 0 && /\w/.test(line.charAt(start - 1))) --start;
  902 + while (end < line.length && /\w/.test(line.charAt(end))) ++end;
  903 + setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end});
  904 + }
  905 + function selectLine(line) {
  906 + setSelectionUser({line: line, ch: 0}, {line: line, ch: lines[line].text.length});
  907 + }
  908 + function handleEnter() {
  909 + replaceSelection("\n", "end");
  910 + if (options.enterMode != "flat")
  911 + indentLine(sel.from.line, options.enterMode == "keep" ? "prev" : "smart");
  912 + }
  913 + function handleTab(shift) {
  914 + shiftSelecting = null;
  915 + switch (options.tabMode) {
  916 + case "default":
  917 + return false;
  918 + case "indent":
  919 + for (var i = sel.from.line, e = sel.to.line; i <= e; ++i) indentLine(i, "smart");
  920 + break;
  921 + case "classic":
  922 + if (posEq(sel.from, sel.to)) {
  923 + if (shift) indentLine(sel.from.line, "smart");
  924 + else replaceSelection("\t", "end");
  925 + break;
  926 + }
  927 + case "shift":
  928 + for (var i = sel.from.line, e = sel.to.line; i <= e; ++i) indentLine(i, shift ? "subtract" : "add");
  929 + break;
  930 + }
  931 + return true;
  932 + }
  933 +
  934 + function indentLine(n, how) {
  935 + if (how == "smart") {
  936 + if (!mode.indent) how = "prev";
  937 + else var state = getStateBefore(n);
  938 + }
  939 +
  940 + var line = lines[n], curSpace = line.indentation(), curSpaceString = line.text.match(/^\s*/)[0], indentation;
  941 + if (how == "prev") {
  942 + if (n) indentation = lines[n-1].indentation();
  943 + else indentation = 0;
  944 + }
  945 + else if (how == "smart") indentation = mode.indent(state, line.text.slice(curSpaceString.length));
  946 + else if (how == "add") indentation = curSpace + options.indentUnit;
  947 + else if (how == "subtract") indentation = curSpace - options.indentUnit;