diff --git a/rhodecode/public/css/codemirror.css b/rhodecode/public/css/codemirror.css --- a/rhodecode/public/css/codemirror.css +++ b/rhodecode/public/css/codemirror.css @@ -1,174 +1,246 @@ +/* BASICS */ + .CodeMirror { - line-height: 1em; + /* Set height, width, borders, and global font properties here */ font-family: monospace; + height: 300px; +} +.CodeMirror-scroll { + /* Set scrolling behaviour here */ + overflow: auto; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; +} + +/* CURSOR */ - /* Necessary so the scrollbar can be absolutely positioned within the wrapper on Lion. */ +.CodeMirror div.CodeMirror-cursor { + border-left: 1px solid black; + z-index: 3; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor { + width: auto; + border: 0; + background: #7e7; + z-index: 1; +} +/* Can style cursor different in overwrite (non-insert) mode */ +.CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {} + +.cm-tab { display: inline-block; } + +/* DEFAULT THEME */ + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable {color: black;} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-property {color: black;} +.cm-s-default .cm-operator {color: black;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-error {color: #f00;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} + +.cm-invalidchar {color: #f00;} + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + line-height: 1; position: relative; - /* This prevents unwanted scrollbars from showing up on the body and wrapper in IE. */ overflow: hidden; + background: white; + color: black; } .CodeMirror-scroll { - overflow: auto; - height: 300px; - /* This is needed to prevent an IE[67] bug where the scrolled content - is visible outside of the scrolling box. */ + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; padding-right: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ position: relative; - outline: none; +} +.CodeMirror-sizer { + position: relative; } -/* Vertical scrollbar */ -.CodeMirror-scrollbar { +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actuall scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler { position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { right: 0; top: 0; overflow-x: hidden; overflow-y: scroll; - z-index: 5; -} -.CodeMirror-scrollbar-inner { - /* This needs to have a nonzero width in order for the scrollbar to appear - in Firefox and IE9. */ - width: 1px; } -.CodeMirror-scrollbar.cm-sb-overlap { - /* Ensure that the scrollbar appears in Lion, and that it overlaps the content - rather than sitting to the right of it. */ - position: absolute; - z-index: 1; - float: none; - right: 0; - min-width: 12px; +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; } -.CodeMirror-scrollbar.cm-sb-nonoverlap { - min-width: 12px; -} -.CodeMirror-scrollbar.cm-sb-ie7 { - min-width: 18px; +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; + z-index: 6; } -.CodeMirror-gutter { +.CodeMirror-gutters { position: absolute; left: 0; top: 0; - z-index: 10; - background-color: #f7f7f7; - border-right: 1px solid #eee; - min-width: 2em; height: 100%; + padding-bottom: 30px; + z-index: 3; } -.CodeMirror-gutter-text { - color: #aaa; - text-align: right; - padding: .4em .2em .4em .4em; - white-space: pre !important; +.CodeMirror-gutter { + height: 100%; + padding-bottom: 30px; + margin-bottom: -32px; + display: inline-block; + /* Hack to make IE7 behave */ + *zoom:1; + *display:inline; +} +.CodeMirror-gutter-elt { + position: absolute; cursor: default; + z-index: 4; } + .CodeMirror-lines { - padding: .4em; - white-space: pre; cursor: text; } - .CodeMirror pre { - -moz-border-radius: 0; - -webkit-border-radius: 0; - -o-border-radius: 0; - border-radius: 0; - border-width: 0; margin: 0; padding: 0; background: transparent; + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; font-family: inherit; font-size: inherit; - padding: 0; margin: 0; + margin: 0; white-space: pre; word-wrap: normal; line-height: inherit; color: inherit; + z-index: 2; + position: relative; overflow: visible; } - .CodeMirror-wrap pre { word-wrap: break-word; white-space: pre-wrap; word-break: normal; } +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +.CodeMirror-widget { + display: inline-block; +} + .CodeMirror-wrap .CodeMirror-scroll { overflow-x: hidden; } -.CodeMirror textarea { - outline: none !important; +.CodeMirror-measure { + position: absolute; + width: 100%; height: 0px; + overflow: hidden; + visibility: hidden; } +.CodeMirror-measure pre { position: static; } -.CodeMirror pre.CodeMirror-cursor { - z-index: 10; +.CodeMirror div.CodeMirror-cursor { position: absolute; visibility: hidden; - border-left: 1px solid black; border-right: none; width: 0; } -.cm-keymap-fat-cursor pre.CodeMirror-cursor { - width: auto; - border: 0; - background: transparent; - background: rgba(0, 200, 0, .4); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#6600c800, endColorstr=#4c00c800); -} -/* Kludge to turn off filter in ie9+, which also accepts rgba */ -.cm-keymap-fat-cursor pre.CodeMirror-cursor:not(#nonsense_id) { - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); -} -.CodeMirror pre.CodeMirror-cursor.CodeMirror-overwrite {} -.CodeMirror-focused pre.CodeMirror-cursor { +.CodeMirror-focused div.CodeMirror-cursor { visibility: visible; } -div.CodeMirror-selected { background: #d9d9d9; } -.CodeMirror-focused div.CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } -.CodeMirror-searching { +.cm-searching { background: #ffa; background: rgba(255, 255, 0, .4); } -/* Default theme */ - -.cm-s-default span.cm-keyword {color: #708;} -.cm-s-default span.cm-atom {color: #219;} -.cm-s-default span.cm-number {color: #164;} -.cm-s-default span.cm-def {color: #00f;} -.cm-s-default span.cm-variable {color: black;} -.cm-s-default span.cm-variable-2 {color: #05a;} -.cm-s-default span.cm-variable-3 {color: #085;} -.cm-s-default span.cm-property {color: black;} -.cm-s-default span.cm-operator {color: black;} -.cm-s-default span.cm-comment {color: #a50;} -.cm-s-default span.cm-string {color: #a11;} -.cm-s-default span.cm-string-2 {color: #f50;} -.cm-s-default span.cm-meta {color: #555;} -.cm-s-default span.cm-error {color: #f00;} -.cm-s-default span.cm-qualifier {color: #555;} -.cm-s-default span.cm-builtin {color: #30a;} -.cm-s-default span.cm-bracket {color: #997;} -.cm-s-default span.cm-tag {color: #170;} -.cm-s-default span.cm-attribute {color: #00c;} -.cm-s-default span.cm-header {color: blue;} -.cm-s-default span.cm-quote {color: #090;} -.cm-s-default span.cm-hr {color: #999;} -.cm-s-default span.cm-link {color: #00c;} - -span.cm-header, span.cm-strong {font-weight: bold;} -span.cm-em {font-style: italic;} -span.cm-emstrong {font-style: italic; font-weight: bold;} -span.cm-link {text-decoration: underline;} - -span.cm-invalidchar {color: #f00;} - -div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} -div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +.CodeMirror span { *vertical-align: text-bottom; } @media print { - /* Hide the cursor when printing */ - .CodeMirror pre.CodeMirror-cursor { + .CodeMirror div.CodeMirror-cursor { visibility: hidden; } - } diff --git a/rhodecode/public/js/codemirror.js b/rhodecode/public/js/codemirror.js --- a/rhodecode/public/js/codemirror.js +++ b/rhodecode/public/js/codemirror.js @@ -1,2039 +1,3154 @@ -// All functions that need access to the editor's state live inside -// the CodeMirror function. Below that, at the bottom of the file, -// some utilities are defined. - // CodeMirror is the only global var we claim window.CodeMirror = (function() { "use strict"; - // This is the function that produces an editor instance. Its - // closure is used to store the editor state. - function CodeMirror(place, givenOptions) { + + // BROWSER SNIFFING + + // Crude, but necessary to handle a number of hard-to-feature-detect + // bugs and behavior differences. + var gecko = /gecko\/\d/i.test(navigator.userAgent); + var ie = /MSIE \d/.test(navigator.userAgent); + var ie_lt8 = ie && (document.documentMode == null || document.documentMode < 8); + var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9); + var webkit = /WebKit\//.test(navigator.userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); + var chrome = /Chrome\//.test(navigator.userAgent); + var opera = /Opera\//.test(navigator.userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var khtml = /KHTML\//.test(navigator.userAgent); + var mac_geLion = /Mac OS X 1\d\D([7-9]|\d\d)\D/.test(navigator.userAgent); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); + var phantom = /PhantomJS/.test(navigator.userAgent); + + var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); + var mac = ios || /Mac/.test(navigator.platform); + var windows = /windows/i.test(navigator.platform); + + var opera_version = opera && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + if (opera_version) opera_version = Number(opera_version[1]); + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || opera && (opera_version == null || opera_version < 12.11)); + var captureMiddleClick = gecko || (ie && !ie_lt9); + + // Optimize some code when these features are not used + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + // CONSTRUCTOR + + function CodeMirror(place, options) { + if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); + + this.options = options = options || {}; // Determine effective options based on given values and defaults. - var options = {}, defaults = CodeMirror.defaults; - for (var opt in defaults) - if (defaults.hasOwnProperty(opt)) - options[opt] = (givenOptions && givenOptions.hasOwnProperty(opt) ? givenOptions : defaults)[opt]; - - var input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em"); - input.setAttribute("wrap", "off"); input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); + for (var opt in defaults) if (!options.hasOwnProperty(opt) && defaults.hasOwnProperty(opt)) + options[opt] = defaults[opt]; + setGuttersForLineNumbers(options); + + var docStart = typeof options.value == "string" ? 0 : options.value.first; + var display = this.display = makeDisplay(place, docStart); + display.wrapper.CodeMirror = this; + updateGutters(this); + if (options.autofocus && !mobile) focusInput(this); + + this.state = {keyMaps: [], + overlays: [], + modeGen: 0, + overwrite: false, focused: false, + suppressEdits: false, pasteIncoming: false, + draggingText: false, + highlight: new Delayed()}; + + themeChanged(this); + if (options.lineWrapping) + this.display.wrapper.className += " CodeMirror-wrap"; + + var doc = options.value; + if (typeof doc == "string") doc = new Doc(options.value, options.mode); + operation(this, attachDoc)(this, doc); + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie) setTimeout(bind(resetInput, this, true), 20); + + registerEventHandlers(this); + // IE throws unspecified error in certain cases, when + // trying to access activeElement before onload + var hasFocus; try { hasFocus = (document.activeElement == display.input); } catch(e) { } + if (hasFocus || (options.autofocus && !mobile)) setTimeout(bind(onFocus, this), 20); + else onBlur(this); + + operation(this, function() { + for (var opt in optionHandlers) + if (optionHandlers.propertyIsEnumerable(opt)) + optionHandlers[opt](this, options[opt], Init); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); + })(); + } + + // DISPLAY CONSTRUCTOR + + function makeDisplay(place, docStart) { + var d = {}; + + var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none; font-size: 4px;"); + if (webkit) input.style.width = "1000px"; + else input.setAttribute("wrap", "off"); + // if border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) input.style.border = "1px solid black"; + input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); + // Wraps and hides input textarea - var inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); - // The empty scrollbar content, used solely for managing the scrollbar thumb. - var scrollbarInner = elt("div", null, "CodeMirror-scrollbar-inner"); - // The vertical scrollbar. Horizontal scrolling is handled by the scroller itself. - var scrollbar = elt("div", [scrollbarInner], "CodeMirror-scrollbar"); + d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The actual fake scrollbars. + d.scrollbarH = elt("div", [elt("div", null, null, "height: 1px")], "CodeMirror-hscrollbar"); + d.scrollbarV = elt("div", [elt("div", null, null, "width: 1px")], "CodeMirror-vscrollbar"); + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); // DIVs containing the selection and the actual code - var lineDiv = elt("div"), selectionDiv = elt("div", null, null, "position: relative; z-index: -1"); + d.lineDiv = elt("div"); + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); // Blinky cursor, and element used to ensure cursor fits at the end of a line - var cursor = elt("pre", "\u00a0", "CodeMirror-cursor"), widthForcer = elt("pre", "\u00a0", "CodeMirror-cursor", "visibility: hidden"); + d.cursor = elt("div", "\u00a0", "CodeMirror-cursor"); + // Secondary cursor, shown when on a 'jump' in bi-directional text + d.otherCursor = elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor"); // Used to measure text size - var measure = elt("div", null, null, "position: absolute; width: 100%; height: 0px; overflow: hidden; visibility: hidden;"); - var lineSpace = elt("div", [measure, cursor, widthForcer, selectionDiv, lineDiv], null, "position: relative; z-index: 0"); - var gutterText = elt("div", null, "CodeMirror-gutter-text"), gutter = elt("div", [gutterText], "CodeMirror-gutter"); + d.measure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = elt("div", [d.measure, d.selectionDiv, d.lineDiv, d.cursor, d.otherCursor], + null, "position: relative; outline: none"); // Moved around its parent to cover visible view - var mover = elt("div", [gutter, elt("div", [lineSpace], "CodeMirror-lines")], null, "position: relative"); + d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); // Set to the height of the text, causes scrolling - var sizer = elt("div", [mover], null, "position: relative"); + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + // D is needed because behavior of elts with overflow: auto and padding is inconsistent across browsers + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerCutOff + "px; width: 1px;"); + // Will contain the gutters, if any + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Helper element to properly size the gutter backgrounds + var scrollerInner = elt("div", [d.sizer, d.heightForcer, d.gutters], null, "position: relative; min-height: 100%"); // Provides scrolling - var scroller = elt("div", [sizer], "CodeMirror-scroll"); - scroller.setAttribute("tabIndex", "-1"); + d.scroller = elt("div", [scrollerInner], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); // The element in which the editor lives. - var wrapper = elt("div", [inputDiv, scrollbar, scroller], "CodeMirror" + (options.lineWrapping ? " CodeMirror-wrap" : "")); - if (place.appendChild) place.appendChild(wrapper); else place(wrapper); - - themeChanged(); keyMapChanged(); + d.wrapper = elt("div", [d.inputDiv, d.scrollbarH, d.scrollbarV, + d.scrollbarFiller, d.scroller], "CodeMirror"); + // Work around IE7 z-index bug + if (ie_lt8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (place.appendChild) place.appendChild(d.wrapper); else place(d.wrapper); + // Needed to hide big blue blinking cursor on Mobile Safari if (ios) input.style.width = "0px"; - if (!webkit) scroller.draggable = true; - lineSpace.style.outline = "none"; - if (options.tabindex != null) input.tabIndex = options.tabindex; - if (options.autofocus) focusInput(); - if (!options.gutter && !options.lineNumbers) gutter.style.display = "none"; + if (!webkit) d.scroller.draggable = true; // Needed to handle Tab key in KHTML - if (khtml) inputDiv.style.height = "1px", inputDiv.style.position = "absolute"; - - // Check for OS X >= 10.7. This has transparent scrollbars, so the - // overlaying of one scrollbar with another won't work. This is a - // temporary hack to simply turn off the overlay scrollbar. See - // issue #727. - if (mac_geLion) { scrollbar.style.zIndex = -2; scrollbar.style.visibility = "hidden"; } + if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; } // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). - else if (ie_lt8) scrollbar.style.minWidth = "18px"; - - // Delayed object wrap timeouts, making sure only one is active. blinker holds an interval. - var poll = new Delayed(), highlight = new Delayed(), blinker; - - // mode holds a mode API object. doc is the tree of Line objects, - // frontier is the point up to which the content has been parsed, - // and history the undo history (instance of History constructor). - var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), frontier = 0, focused; - loadMode(); - // The selection. These are always maintained to point at valid - // positions. Inverted is used to remember that the user is - // selecting bottom-to-top. - var sel = {from: {line: 0, ch: 0}, to: {line: 0, ch: 0}, inverted: false}; - // Selection-related flags. shiftSelecting obviously tracks - // whether the user is holding shift. - var shiftSelecting, lastClick, lastDoubleClick, lastScrollTop = 0, draggingText, - overwrite = false, suppressEdits = false, pasteIncoming = false; - // Variables used by startOperation/endOperation to track what - // happened during the operation. - var updateInput, userSelChange, changes, textChanged, selectionChanged, - gutterDirty, callbacks; + else if (ie_lt8) d.scrollbarH.style.minWidth = d.scrollbarV.style.minWidth = "18px"; + // Current visible range (may be bigger than the view window). - var displayOffset = 0, showingFrom = 0, showingTo = 0, lastSizeC = 0; - // bracketHighlighted is used to remember that a bracket has been - // marked. - var bracketHighlighted; + d.viewOffset = d.lastSizeC = 0; + d.showingFrom = d.showingTo = docStart; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // See readInput and resetInput + d.prevInput = ""; + // Set to true when a non-horizontal-scrolling widget is added. As + // an optimization, widget aligning is skipped when d is false. + d.alignWidgets = false; + // Flag that indicates whether we currently expect input to appear + // (after some event like 'keypress' or 'input') and are polling + // intensively. + d.pollingFast = false; + // Self-resetting timeout for the poller + d.poll = new Delayed(); + + d.cachedCharWidth = d.cachedTextHeight = null; + d.measureLineCache = []; + d.measureLineCachePos = 0; + + // Tracks when resetInput has punted to just putting a short + // string instead of the (large) selection. + d.inaccurateSelection = false; + // Tracks the maximum line length so that the horizontal scrollbar // can be kept static when scrolling. - var maxLine = getLine(0), updateMaxLine = false, maxLineChanged = true; - var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll - var goalColumn = null; - - // Initialize the content. - operation(function(){setValue(options.value || ""); updateInput = false;})(); - var history = new History(); - - // Register our event handlers. - connect(scroller, "mousedown", operation(onMouseDown)); - connect(scroller, "dblclick", operation(onDoubleClick)); - connect(lineSpace, "selectstart", e_preventDefault); + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + return d; + } + + // STATE UPDATES + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); + cm.doc.iter(function(line) { + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + }); + cm.doc.frontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) regChange(cm); + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + cm.display.wrapper.className += " CodeMirror-wrap"; + cm.display.sizer.style.minWidth = ""; + } else { + cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-wrap", ""); + computeMaxLength(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function(){updateScrollbars(cm.display, cm.doc.height);}, 100); + } + + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function(line) { + if (lineIsHidden(cm.doc, line)) + return 0; + else if (wrapping) + return (Math.ceil(line.text.length / perLine) || 1) * th; + else + return th; + }; + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function(line) { + var estHeight = est(line); + if (estHeight != line.height) updateLineHeight(line, estHeight); + }); + } + + function keyMapChanged(cm) { + var style = keyMap[cm.options.keyMap].style; + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-keymap-\S+/g, "") + + (style ? " cm-keymap-" + style : ""); + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + function guttersChanged(cm) { + updateGutters(cm); + regChange(cm); + } + + function updateGutters(cm) { + var gutters = cm.display.gutters, specs = cm.options.gutters; + removeChildren(gutters); + for (var i = 0; i < specs.length; ++i) { + var gutterClass = specs[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + cm.display.lineGutter = gElt; + gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = i ? "" : "none"; + } + + function lineLength(doc, line) { + if (line.height == 0) return 0; + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(); + cur = getLine(doc, found.from.line); + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found = merged.find(); + len -= cur.text.length - found.from.ch; + cur = getLine(doc, found.to.line); + len += cur.text.length - found.to.ch; + } + return len; + } + + function computeMaxLength(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(doc, d.maxLine); + d.maxLineChanged = true; + doc.iter(function(line) { + var len = lineLength(doc, line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // Make sure the gutters options contains the element + // "CodeMirror-linenumbers" when the lineNumbers option is true. + function setGuttersForLineNumbers(options) { + var found = false; + for (var i = 0; i < options.gutters.length; ++i) { + if (options.gutters[i] == "CodeMirror-linenumbers") { + if (options.lineNumbers) found = true; + else options.gutters.splice(i--, 1); + } + } + if (!found && options.lineNumbers) + options.gutters.push("CodeMirror-linenumbers"); + } + + // SCROLLBARS + + // Re-synchronize the fake scrollbars with the actual size of the + // content. Optionally force a scrollTop. + function updateScrollbars(d /* display */, docHeight) { + var totalHeight = docHeight + paddingVert(d); + d.sizer.style.minHeight = d.heightForcer.style.top = totalHeight + "px"; + var scrollHeight = Math.max(totalHeight, d.scroller.scrollHeight); + var needsH = d.scroller.scrollWidth > d.scroller.clientWidth; + var needsV = scrollHeight > d.scroller.clientHeight; + if (needsV) { + d.scrollbarV.style.display = "block"; + d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0"; + d.scrollbarV.firstChild.style.height = + (scrollHeight - d.scroller.clientHeight + d.scrollbarV.clientHeight) + "px"; + } else d.scrollbarV.style.display = ""; + if (needsH) { + d.scrollbarH.style.display = "block"; + d.scrollbarH.style.right = needsV ? scrollbarWidth(d.measure) + "px" : "0"; + d.scrollbarH.firstChild.style.width = + (d.scroller.scrollWidth - d.scroller.clientWidth + d.scrollbarH.clientWidth) + "px"; + } else d.scrollbarH.style.display = ""; + if (needsH && needsV) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = d.scrollbarFiller.style.width = scrollbarWidth(d.measure) + "px"; + } else d.scrollbarFiller.style.display = ""; + + if (mac_geLion && scrollbarWidth(d.measure) === 0) + d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = mac_geMountainLion ? "18px" : "12px"; + } + + function visibleLines(display, doc, viewPort) { + var top = display.scroller.scrollTop, height = display.wrapper.clientHeight; + if (typeof viewPort == "number") top = viewPort; + else if (viewPort) {top = viewPort.top; height = viewPort.bottom - viewPort.top;} + top = Math.floor(top - paddingTop(display)); + var bottom = Math.ceil(top + height); + return {from: lineAtHeight(doc, top), to: lineAtHeight(doc, bottom)}; + } + + // LINE NUMBERS + + function alignHorizontally(cm) { + var display = cm.display; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, l = comp + "px"; + for (var n = display.lineDiv.firstChild; n; n = n.nextSibling) if (n.alignable) { + for (var i = 0, a = n.alignable; i < a.length; ++i) a[i].style.left = l; + } + if (cm.options.fixedGutter) + display.gutters.style.left = (comp + gutterW) + "px"; + } + + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) return false; + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding); + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + return true; + } + return false; + } + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)); + } + function compensateForHScroll(display) { + return getRect(display.scroller).left - getRect(display.sizer).left; + } + + // DISPLAY DRAWING + + function updateDisplay(cm, changes, viewPort) { + var oldFrom = cm.display.showingFrom, oldTo = cm.display.showingTo, updated; + var visible = visibleLines(cm.display, cm.doc, viewPort); + for (;;) { + if (updateDisplayInner(cm, changes, visible)) { + updated = true; + signalLater(cm, "update", cm); + if (cm.display.showingFrom != oldFrom || cm.display.showingTo != oldTo) + signalLater(cm, "viewportChange", cm, cm.display.showingFrom, cm.display.showingTo); + } else break; + updateSelection(cm); + updateScrollbars(cm.display, cm.doc.height); + + // Clip forced viewport to actual scrollable area + if (viewPort) + viewPort = Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, + typeof viewPort == "number" ? viewPort : viewPort.top); + visible = visibleLines(cm.display, cm.doc, viewPort); + if (visible.from >= cm.display.showingFrom && visible.to <= cm.display.showingTo) + break; + changes = []; + } + + return updated; + } + + // Uses a set of changes plus the current scroll position to + // determine which DOM updates have to be made, and makes the + // updates. + function updateDisplayInner(cm, changes, visible) { + var display = cm.display, doc = cm.doc; + if (!display.wrapper.clientWidth) { + display.showingFrom = display.showingTo = doc.first; + display.viewOffset = 0; + return; + } + + // Bail out if the visible area is already rendered and nothing changed. + if (changes.length == 0 && + visible.from > display.showingFrom && visible.to < display.showingTo) + return; + + if (maybeUpdateLineNumberWidth(cm)) + changes = [{from: doc.first, to: doc.first + doc.size}]; + var gutterW = display.sizer.style.marginLeft = display.gutters.offsetWidth + "px"; + display.scrollbarH.style.left = cm.options.fixedGutter ? gutterW : "0"; + + // Used to determine which lines need their line numbers updated + var positionsChangedFrom = Infinity; + if (cm.options.lineNumbers) + for (var i = 0; i < changes.length; ++i) + if (changes[i].diff) { positionsChangedFrom = changes[i].from; break; } + + var end = doc.first + doc.size; + var from = Math.max(visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, visible.to + cm.options.viewportMargin); + if (display.showingFrom < from && from - display.showingFrom < 20) from = Math.max(doc.first, display.showingFrom); + if (display.showingTo > to && display.showingTo - to < 20) to = Math.min(end, display.showingTo); + if (sawCollapsedSpans) { + from = lineNo(visualLine(doc, getLine(doc, from))); + while (to < end && lineIsHidden(doc, getLine(doc, to))) ++to; + } + + // Create a range of theoretically intact lines, and punch holes + // in that using the change info. + var intact = [{from: Math.max(display.showingFrom, doc.first), + to: Math.min(display.showingTo, end)}]; + if (intact[0].from >= intact[0].to) intact = []; + else intact = computeIntact(intact, changes); + // When merged lines are present, we might have to reduce the + // intact ranges because changes in continued fragments of the + // intact lines do require the lines to be redrawn. + if (sawCollapsedSpans) + for (var i = 0; i < intact.length; ++i) { + var range = intact[i], merged; + while (merged = collapsedSpanAtEnd(getLine(doc, range.to - 1))) { + var newTo = merged.find().from.line; + if (newTo > range.from) range.to = newTo; + else { intact.splice(i--, 1); break; } + } + } + + // Clip off the parts that won't be visible + var intactLines = 0; + for (var i = 0; i < intact.length; ++i) { + var range = intact[i]; + if (range.from < from) range.from = from; + if (range.to > to) range.to = to; + if (range.from >= range.to) intact.splice(i--, 1); + else intactLines += range.to - range.from; + } + if (intactLines == to - from && from == display.showingFrom && to == display.showingTo) { + updateViewOffset(cm); + return; + } + intact.sort(function(a, b) {return a.from - b.from;}); + + // Avoid crashing on IE's "unspecified error" when in iframes + try { + var focused = document.activeElement; + } catch(e) {} + if (intactLines < (to - from) * .7) display.lineDiv.style.display = "none"; + patchDisplay(cm, from, to, intact, positionsChangedFrom); + display.lineDiv.style.display = ""; + if (focused && document.activeElement != focused && focused.offsetHeight) focused.focus(); + + var different = from != display.showingFrom || to != display.showingTo || + display.lastSizeC != display.wrapper.clientHeight; + // This is just a bogus formula that detects when the editor is + // resized or the font size changes. + if (different) display.lastSizeC = display.wrapper.clientHeight; + display.showingFrom = from; display.showingTo = to; + startWorker(cm, 100); + + var prevBottom = display.lineDiv.offsetTop; + for (var node = display.lineDiv.firstChild, height; node; node = node.nextSibling) if (node.lineObj) { + if (ie_lt8) { + var bot = node.offsetTop + node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = getRect(node); + height = box.bottom - box.top; + } + var diff = node.lineObj.height - height; + if (height < 2) height = textHeight(display); + if (diff > .001 || diff < -.001) { + updateLineHeight(node.lineObj, height); + var widgets = node.lineObj.widgets; + if (widgets) for (var i = 0; i < widgets.length; ++i) + widgets[i].height = widgets[i].node.offsetHeight; + } + } + updateViewOffset(cm); + + return true; + } + + function updateViewOffset(cm) { + var off = cm.display.viewOffset = heightAtLine(cm, getLine(cm.doc, cm.display.showingFrom)); + // Position the mover div to align with the current virtual scroll position + cm.display.mover.style.top = off + "px"; + } + + function computeIntact(intact, changes) { + for (var i = 0, l = changes.length || 0; i < l; ++i) { + var change = changes[i], intact2 = [], diff = change.diff || 0; + for (var j = 0, l2 = intact.length; j < l2; ++j) { + var range = intact[j]; + if (change.to <= range.from && change.diff) { + intact2.push({from: range.from + diff, to: range.to + diff}); + } else if (change.to <= range.from || change.from >= range.to) { + intact2.push(range); + } else { + if (change.from > range.from) + intact2.push({from: range.from, to: change.from}); + if (change.to < range.to) + intact2.push({from: change.to + diff, to: range.to + diff}); + } + } + intact = intact2; + } + return intact; + } + + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + left[cm.options.gutters[i]] = n.offsetLeft; + width[cm.options.gutters[i]] = n.offsetWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth}; + } + + function patchDisplay(cm, from, to, intact, updateNumbersFrom) { + var dims = getDimensions(cm); + var display = cm.display, lineNumbers = cm.options.lineNumbers; + if (!intact.length && (!webkit || !cm.display.currentWheelTarget)) + removeChildren(display.lineDiv); + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + if (webkit && mac && cm.display.currentWheelTarget == node) { + node.style.display = "none"; + node.lineObj = null; + } else { + node.parentNode.removeChild(node); + } + return next; + } + + var nextIntact = intact.shift(), lineN = from; + cm.doc.iter(from, to, function(line) { + if (nextIntact && nextIntact.to == lineN) nextIntact = intact.shift(); + if (lineIsHidden(cm.doc, line)) { + if (line.height != 0) updateLineHeight(line, 0); + if (line.widgets && cur.previousSibling) for (var i = 0; i < line.widgets.length; ++i) + if (line.widgets[i].showIfHidden) { + var prev = cur.previousSibling; + if (/pre/i.test(prev.nodeName)) { + var wrap = elt("div", null, null, "position: relative"); + prev.parentNode.replaceChild(wrap, prev); + wrap.appendChild(prev); + prev = wrap; + } + var wnode = prev.appendChild(elt("div", [line.widgets[i].node], "CodeMirror-linewidget")); + positionLineWidget(line.widgets[i], wnode, prev, dims); + } + } else if (nextIntact && nextIntact.from <= lineN && nextIntact.to > lineN) { + // This line is intact. Skip to the actual node. Update its + // line number if needed. + while (cur.lineObj != line) cur = rm(cur); + if (lineNumbers && updateNumbersFrom <= lineN && cur.lineNumber) + setTextContent(cur.lineNumber, lineNumberFor(cm.options, lineN)); + cur = cur.nextSibling; + } else { + // For lines with widgets, make an attempt to find and reuse + // the existing element, so that widgets aren't needlessly + // removed and re-inserted into the dom + if (line.widgets) for (var j = 0, search = cur, reuse; search && j < 20; ++j, search = search.nextSibling) + if (search.lineObj == line && /div/i.test(search.nodeName)) { reuse = search; break; } + // This line needs to be generated. + var lineNode = buildLineElement(cm, line, lineN, dims, reuse); + if (lineNode != reuse) { + container.insertBefore(lineNode, cur); + } else { + while (cur != reuse) cur = rm(cur); + cur = cur.nextSibling; + } + + lineNode.lineObj = line; + } + ++lineN; + }); + while (cur) cur = rm(cur); + } + + function buildLineElement(cm, line, lineNo, dims, reuse) { + var lineElement = lineContent(cm, line); + var markers = line.gutterMarkers, display = cm.display, wrap; + + if (!cm.options.lineNumbers && !markers && !line.bgClass && !line.wrapClass && !line.widgets) + return lineElement; + + // Lines with gutter elements, widgets or a background class need + // to be wrapped again, and have the extra elements added to the + // wrapper div + + if (reuse) { + reuse.alignable = null; + var isOk = true, widgetsSeen = 0; + for (var n = reuse.firstChild, next; n; n = next) { + next = n.nextSibling; + if (!/\bCodeMirror-linewidget\b/.test(n.className)) { + reuse.removeChild(n); + } else { + for (var i = 0, first = true; i < line.widgets.length; ++i) { + var widget = line.widgets[i], isFirst = false; + if (!widget.above) { isFirst = first; first = false; } + if (widget.node == n.firstChild) { + positionLineWidget(widget, n, reuse, dims); + ++widgetsSeen; + if (isFirst) reuse.insertBefore(lineElement, n); + break; + } + } + if (i == line.widgets.length) { isOk = false; break; } + } + } + if (isOk && widgetsSeen == line.widgets.length) { + wrap = reuse; + reuse.className = line.wrapClass || ""; + } + } + if (!wrap) { + wrap = elt("div", null, line.wrapClass, "position: relative"); + wrap.appendChild(lineElement); + } + // Kludge to make sure the styled element lies behind the selection (by z-index) + if (line.bgClass) + wrap.insertBefore(elt("div", null, line.bgClass + " CodeMirror-linebackground"), wrap.firstChild); + if (cm.options.lineNumbers || markers) { + var gutterWrap = wrap.insertBefore(elt("div", null, null, "position: absolute; left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"), + wrap.firstChild); + if (cm.options.fixedGutter) (wrap.alignable || (wrap.alignable = [])).push(gutterWrap); + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + wrap.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineNo), + "CodeMirror-linenumber CodeMirror-gutter-elt", + "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " + + display.lineNumInnerWidth + "px")); + if (markers) + for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); + } + } + if (ie_lt8) wrap.style.zIndex = 2; + if (line.widgets && wrap != reuse) for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); + positionLineWidget(widget, node, wrap, dims); + if (widget.above) + wrap.insertBefore(node, cm.options.lineNumbers && line.height != 0 ? gutterWrap : lineElement); + else + wrap.appendChild(node); + signalLater(widget, "redraw"); + } + return wrap; + } + + function positionLineWidget(widget, node, wrap, dims) { + if (widget.noHScroll) { + (wrap.alignable || (wrap.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; + } + } + + // SELECTION / CURSOR + + function updateSelection(cm) { + var display = cm.display; + var collapsed = posEq(cm.doc.sel.from, cm.doc.sel.to); + if (collapsed || cm.options.showCursorWhenSelecting) + updateSelectionCursor(cm); + else + display.cursor.style.display = display.otherCursor.style.display = "none"; + if (!collapsed) + updateSelectionRange(cm); + else + display.selectionDiv.style.display = "none"; + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, cm.doc.sel.head, "div"); + var wrapOff = getRect(display.wrapper), lineOff = getRect(display.lineDiv); + display.inputDiv.style.top = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)) + "px"; + display.inputDiv.style.left = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)) + "px"; + } + } + + // No selection, plain cursor + function updateSelectionCursor(cm) { + var display = cm.display, pos = cursorCoords(cm, cm.doc.sel.head, "div"); + display.cursor.style.left = pos.left + "px"; + display.cursor.style.top = pos.top + "px"; + display.cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + display.cursor.style.display = ""; + + if (pos.other) { + display.otherCursor.style.display = ""; + display.otherCursor.style.left = pos.other.left + "px"; + display.otherCursor.style.top = pos.other.top + "px"; + display.otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } else { display.otherCursor.style.display = "none"; } + } + + // Highlight selection + function updateSelectionRange(cm) { + var display = cm.display, doc = cm.doc, sel = cm.doc.sel; + var fragment = document.createDocumentFragment(); + var clientWidth = display.lineSpace.offsetWidth, pl = paddingLeft(cm.display); + + function add(left, top, width, bottom) { + if (top < 0) top = 0; + fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + + "px; top: " + top + "px; width: " + (width == null ? clientWidth - left : width) + + "px; height: " + (bottom - top) + "px")); + } + + function drawForLine(line, fromArg, toArg, retTop) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length, rVal = retTop ? Infinity : -Infinity; + function coords(ch) { + return charCoords(cm, Pos(line, ch), "div", lineObj); + } + + iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { + var leftPos = coords(from), rightPos, left, right; + if (from == to) { + rightPos = leftPos; + left = right = leftPos.left; + } else { + rightPos = coords(to - 1); + if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } + left = leftPos.left; + right = rightPos.right; + } + if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part + add(left, leftPos.top, null, leftPos.bottom); + left = pl; + if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); + } + if (toArg == null && to == lineLen) right = clientWidth; + if (fromArg == null && from == 0) left = pl; + rVal = retTop ? Math.min(rightPos.top, rVal) : Math.max(rightPos.bottom, rVal); + if (left < pl + 1) left = pl; + add(left, rightPos.top, right - left, rightPos.bottom); + }); + return rVal; + } + + if (sel.from.line == sel.to.line) { + drawForLine(sel.from.line, sel.from.ch, sel.to.ch); + } else { + var fromObj = getLine(doc, sel.from.line); + var cur = fromObj, merged, path = [sel.from.line, sel.from.ch], singleLine; + while (merged = collapsedSpanAtEnd(cur)) { + var found = merged.find(); + path.push(found.from.ch, found.to.line, found.to.ch); + if (found.to.line == sel.to.line) { + path.push(sel.to.ch); + singleLine = true; + break; + } + cur = getLine(doc, found.to.line); + } + + // This is a single, merged line + if (singleLine) { + for (var i = 0; i < path.length; i += 3) + drawForLine(path[i], path[i+1], path[i+2]); + } else { + var middleTop, middleBot, toObj = getLine(doc, sel.to.line); + if (sel.from.ch) + // Draw the first line of selection. + middleTop = drawForLine(sel.from.line, sel.from.ch, null, false); + else + // Simply include it in the middle block. + middleTop = heightAtLine(cm, fromObj) - display.viewOffset; + + if (!sel.to.ch) + middleBot = heightAtLine(cm, toObj) - display.viewOffset; + else + middleBot = drawForLine(sel.to.line, collapsedSpanAtStart(toObj) ? null : 0, sel.to.ch, true); + + if (middleTop < middleBot) add(pl, middleTop, null, middleBot); + } + } + + removeChildrenAndAdd(display.selectionDiv, fragment); + display.selectionDiv.style.display = ""; + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) return; + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursor.style.visibility = display.otherCursor.style.visibility = ""; + display.blinker = setInterval(function() { + display.cursor.style.visibility = display.otherCursor.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.showingTo) + cm.state.highlight.set(time, bind(highlightWorker, cm)); + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.frontier < doc.first) doc.frontier = doc.first; + if (doc.frontier >= cm.display.showingTo) return; + var end = +new Date + cm.options.workTime; + var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); + var changed = [], prevChange; + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.showingTo + 500), function(line) { + if (doc.frontier >= cm.display.showingFrom) { // Visible + var oldStyles = line.styles; + line.styles = highlightLine(cm, line, state); + var ischange = !oldStyles || oldStyles.length != line.styles.length; + for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; + if (ischange) { + if (prevChange && prevChange.end == doc.frontier) prevChange.end++; + else changed.push(prevChange = {start: doc.frontier, end: doc.frontier + 1}); + } + line.stateAfter = copyState(doc.mode, state); + } else { + processLine(cm, line, state); + line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; + } + ++doc.frontier; + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true; + } + }); + if (changed.length) + operation(cm, function() { + for (var i = 0; i < changed.length; ++i) + regChange(this, changed[i].start, changed[i].end); + })(); + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n) { + var minindent, minline, doc = cm.doc; + for (var search = n, lim = n - 100; search > lim; --search) { + if (search <= doc.first) return doc.first; + var line = getLine(doc, search - 1); + if (line.stateAfter) return search; + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + + function getStateBefore(cm, n) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) return true; + var pos = findStartLine(cm, n), state = pos > doc.first && getLine(doc, pos-1).stateAfter; + if (!state) state = startState(doc.mode); + else state = copyState(doc.mode, state); + doc.iter(pos, n, function(line) { + processLine(cm, line, state); + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.showingFrom && pos < display.showingTo; + line.stateAfter = save ? copyState(doc.mode, state) : null; + ++pos; + }); + return state; + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop;} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} + function paddingLeft(display) { + var e = removeChildrenAndAdd(display.measure, elt("pre", null, null, "text-align: left")).appendChild(elt("span", "x")); + return e.offsetLeft; + } + + function measureChar(cm, line, ch, data) { + var dir = -1; + data = data || measureLine(cm, line); + + for (var pos = ch;; pos += dir) { + var r = data[pos]; + if (r) break; + if (dir < 0 && pos == 0) dir = 1; + } + return {left: pos < ch ? r.right : r.left, + right: pos > ch ? r.left : r.right, + top: r.top, bottom: r.bottom}; + } + + function findCachedMeasurement(cm, line) { + var cache = cm.display.measureLineCache; + for (var i = 0; i < cache.length; ++i) { + var memo = cache[i]; + if (memo.text == line.text && memo.markedSpans == line.markedSpans && + cm.display.scroller.clientWidth == memo.width && + memo.classes == line.textClass + "|" + line.bgClass + "|" + line.wrapClass) + return memo.measure; + } + } + + function measureLine(cm, line) { + // First look in the cache + var measure = findCachedMeasurement(cm, line); + if (!measure) { + // Failing that, recompute and store result in cache + measure = measureLineInner(cm, line); + var cache = cm.display.measureLineCache; + var memo = {text: line.text, width: cm.display.scroller.clientWidth, + markedSpans: line.markedSpans, measure: measure, + classes: line.textClass + "|" + line.bgClass + "|" + line.wrapClass}; + if (cache.length == 16) cache[++cm.display.measureLineCachePos % 16] = memo; + else cache.push(memo); + } + return measure; + } + + function measureLineInner(cm, line) { + var display = cm.display, measure = emptyArray(line.text.length); + var pre = lineContent(cm, line, measure); + + // IE does not cache element positions of inline elements between + // calls to getBoundingClientRect. This makes the loop below, + // which gathers the positions of all the characters on the line, + // do an amount of layout work quadratic to the number of + // characters. When line wrapping is off, we try to improve things + // by first subdividing the line into a bunch of inline blocks, so + // that IE can reuse most of the layout information from caches + // for those blocks. This does interfere with line wrapping, so it + // doesn't work when wrapping is on, but in that case the + // situation is slightly better, since IE does cache line-wrapping + // information and only recomputes per-line. + if (ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) { + var fragment = document.createDocumentFragment(); + var chunk = 10, n = pre.childNodes.length; + for (var i = 0, chunks = Math.ceil(n / chunk); i < chunks; ++i) { + var wrap = elt("div", null, null, "display: inline-block"); + for (var j = 0; j < chunk && n; ++j) { + wrap.appendChild(pre.firstChild); + --n; + } + fragment.appendChild(wrap); + } + pre.appendChild(fragment); + } + + removeChildrenAndAdd(display.measure, pre); + + var outer = getRect(display.lineDiv); + var vranges = [], data = emptyArray(line.text.length), maxBot = pre.offsetHeight; + // Work around an IE7/8 bug where it will sometimes have randomly + // replaced our pre with a clone at this point. + if (ie_lt9 && display.measure.first != pre) + removeChildrenAndAdd(display.measure, pre); + + for (var i = 0, cur; i < measure.length; ++i) if (cur = measure[i]) { + var size = getRect(cur); + var top = Math.max(0, size.top - outer.top), bot = Math.min(size.bottom - outer.top, maxBot); + for (var j = 0; j < vranges.length; j += 2) { + var rtop = vranges[j], rbot = vranges[j+1]; + if (rtop > bot || rbot < top) continue; + if (rtop <= top && rbot >= bot || + top <= rtop && bot >= rbot || + Math.min(bot, rbot) - Math.max(top, rtop) >= (bot - top) >> 1) { + vranges[j] = Math.min(top, rtop); + vranges[j+1] = Math.max(bot, rbot); + break; + } + } + if (j == vranges.length) vranges.push(top, bot); + var right = size.right; + if (cur.measureRight) right = getRect(cur.measureRight).left; + data[i] = {left: size.left - outer.left, right: right - outer.left, top: j}; + } + for (var i = 0, cur; i < data.length; ++i) if (cur = data[i]) { + var vr = cur.top; + cur.top = vranges[vr]; cur.bottom = vranges[vr+1]; + } + + return data; + } + + function measureLineWidth(cm, line) { + var hasBadSpan = false; + if (line.markedSpans) for (var i = 0; i < line.markedSpans; ++i) { + var sp = line.markedSpans[i]; + if (sp.collapsed && (sp.to == null || sp.to == line.text.length)) hasBadSpan = true; + } + var cached = !hasBadSpan && findCachedMeasurement(cm, line); + if (cached) return measureChar(cm, line, line.text.length, cached).right; + + var pre = lineContent(cm, line); + var end = pre.appendChild(zeroWidthElement(cm.display.measure)); + removeChildrenAndAdd(cm.display.measure, pre); + return getRect(end).right - getRect(cm.display.lineDiv).left; + } + + function clearCaches(cm) { + cm.display.measureLineCache.length = cm.display.measureLineCachePos = 0; + cm.display.cachedCharWidth = cm.display.cachedTextHeight = null; + if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; + cm.display.lineNumChars = null; + } + + // Context is one of "line", "div" (display.lineDiv), "local"/null (editor), or "page" + function intoCoordSystem(cm, lineObj, rect, context) { + if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { + var size = widgetHeight(lineObj.widgets[i]); + rect.top += size; rect.bottom += size; + } + if (context == "line") return rect; + if (!context) context = "local"; + var yOff = heightAtLine(cm, lineObj); + if (context != "local") yOff -= cm.display.viewOffset; + if (context == "page") { + var lOff = getRect(cm.display.lineSpace); + yOff += lOff.top + (window.pageYOffset || (document.documentElement || document.body).scrollTop); + var xOff = lOff.left + (window.pageXOffset || (document.documentElement || document.body).scrollLeft); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect; + } + + // Context may be "window", "page", "div", or "local"/null + // Result is in "div" coords + function fromCoordSystem(cm, coords, context) { + if (context == "div") return coords; + var left = coords.left, top = coords.top; + if (context == "page") { + left -= window.pageXOffset || (document.documentElement || document.body).scrollLeft; + top -= window.pageYOffset || (document.documentElement || document.body).scrollTop; + } + var lineSpaceBox = getRect(cm.display.lineSpace); + left -= lineSpaceBox.left; + top -= lineSpaceBox.top; + if (context == "local" || !context) { + var editorBox = getRect(cm.display.wrapper); + left += editorBox.left; + top += editorBox.top; + } + return {left: left, top: top}; + } + + function charCoords(cm, pos, context, lineObj) { + if (!lineObj) lineObj = getLine(cm.doc, pos.line); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch), context); + } + + function cursorCoords(cm, pos, context, lineObj, measurement) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!measurement) measurement = measureLine(cm, lineObj); + function get(ch, right) { + var m = measureChar(cm, lineObj, ch, measurement); + if (right) m.left = m.right; else m.right = m.left; + return intoCoordSystem(cm, lineObj, m, context); + } + var order = getOrder(lineObj), ch = pos.ch; + if (!order) return get(ch); + var main, other, linedir = order[0].level; + for (var i = 0; i < order.length; ++i) { + var part = order[i], rtl = part.level % 2, nb, here; + if (part.from < ch && part.to > ch) return get(ch, rtl); + var left = rtl ? part.to : part.from, right = rtl ? part.from : part.to; + if (left == ch) { + // IE returns bogus offsets and widths for edges where the + // direction flips, but only for the side with the lower + // level. So we try to use the side with the higher level. + if (i && part.level < (nb = order[i-1]).level) here = get(nb.level % 2 ? nb.from : nb.to - 1, true); + else here = get(rtl && part.from != part.to ? ch - 1 : ch); + if (rtl == linedir) main = here; else other = here; + } else if (right == ch) { + var nb = i < order.length - 1 && order[i+1]; + if (!rtl && nb && nb.from == nb.to) continue; + if (nb && part.level < nb.level) here = get(nb.level % 2 ? nb.to - 1 : nb.from); + else here = get(rtl ? ch : ch - 1, true); + if (rtl == linedir) main = here; else other = here; + } + } + if (linedir && !ch) other = get(order[0].to - 1); + if (!main) return other; + if (other) main.other = other; + return main; + } + + function PosMaybeOutside(line, ch, outside) { + var pos = new Pos(line, ch); + if (outside) pos.outside = true; + return pos; + } + + // Coords must be lineSpace-local + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) return PosMaybeOutside(doc.first, 0, true); + var lineNo = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineNo > last) + return PosMaybeOutside(doc.first + doc.size - 1, getLine(doc, last).text.length, true); + if (x < 0) x = 0; + + for (;;) { + var lineObj = getLine(doc, lineNo); + var found = coordsCharInner(cm, lineObj, lineNo, x, y); + var merged = collapsedSpanAtEnd(lineObj); + var mergedPos = merged && merged.find(); + if (merged && found.ch >= mergedPos.from.ch) + lineNo = mergedPos.to.line; + else + return found; + } + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + var innerOff = y - heightAtLine(cm, lineObj); + var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; + var measurement = measureLine(cm, lineObj); + + function getX(ch) { + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", + lineObj, measurement); + wrongLine = true; + if (innerOff > sp.bottom) return sp.left - adjust; + else if (innerOff < sp.top) return sp.left + adjust; + else wrongLine = false; + return sp.left; + } + + var bidi = getOrder(lineObj), dist = lineObj.text.length; + var from = lineLeft(lineObj), to = lineRight(lineObj); + var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; + + if (x > toX) return PosMaybeOutside(lineNo, to, toOutside); + // Do a binary search between these bounds. + for (;;) { + if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { + var after = x - fromX < toX - x, ch = after ? from : to; + while (isExtendingChar.test(lineObj.text.charAt(ch))) ++ch; + var pos = PosMaybeOutside(lineNo, ch, after ? fromOutside : toOutside); + pos.after = after; + return pos; + } + var step = Math.ceil(dist / 2), middle = from + step; + if (bidi) { + middle = from; + for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); + } + var middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} + else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} + } + } + + var measureText; + function textHeight(display) { + if (display.cachedTextHeight != null) return display.cachedTextHeight; + if (measureText == null) { + measureText = elt("pre"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) display.cachedTextHeight = height; + removeChildren(display.measure); + return height || 1; + } + + function charWidth(display) { + if (display.cachedCharWidth != null) return display.cachedCharWidth; + var anchor = elt("span", "x"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(display.measure, pre); + var width = anchor.offsetWidth; + if (width > 2) display.cachedCharWidth = width; + return width || 10; + } + + // OPERATIONS + + // Operations are used to wrap changes in such a way that each + // change won't have to update the cursor and display (which would + // be awkward, slow, and error-prone), but instead updates are + // batched and then all combined and executed at once. + + var nextOpId = 0; + function startOperation(cm) { + cm.curOp = { + // An array of ranges of lines that have to be updated. See + // updateDisplay. + changes: [], + updateInput: null, + userSelChange: null, + textChanged: null, + selectionChanged: false, + cursorActivity: false, + updateMaxLine: false, + updateScrollPos: false, + id: ++nextOpId + }; + if (!delayedCallbackDepth++) delayedCallbacks = []; + } + + function endOperation(cm) { + var op = cm.curOp, doc = cm.doc, display = cm.display; + cm.curOp = null; + + if (op.updateMaxLine) computeMaxLength(cm); + if (display.maxLineChanged && !cm.options.lineWrapping && display.maxLine) { + var width = measureLineWidth(cm, display.maxLine); + display.sizer.style.minWidth = Math.max(0, width + 3 + scrollerCutOff) + "px"; + display.maxLineChanged = false; + var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + display.sizer.offsetWidth - display.scroller.clientWidth); + if (maxScrollLeft < doc.scrollLeft && !op.updateScrollPos) + setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true); + } + var newScrollPos, updated; + if (op.updateScrollPos) { + newScrollPos = op.updateScrollPos; + } else if (op.selectionChanged && display.scroller.clientHeight) { // don't rescroll if not visible + var coords = cursorCoords(cm, doc.sel.head); + newScrollPos = calculateScrollPos(cm, coords.left, coords.top, coords.left, coords.bottom); + } + if (op.changes.length || newScrollPos && newScrollPos.scrollTop != null) { + updated = updateDisplay(cm, op.changes, newScrollPos && newScrollPos.scrollTop); + if (cm.display.scroller.offsetHeight) cm.doc.scrollTop = cm.display.scroller.scrollTop; + } + if (!updated && op.selectionChanged) updateSelection(cm); + if (op.updateScrollPos) { + display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = newScrollPos.scrollTop; + display.scroller.scrollLeft = display.scrollbarH.scrollLeft = doc.scrollLeft = newScrollPos.scrollLeft; + alignHorizontally(cm); + if (op.scrollToPos) + scrollPosIntoView(cm, clipPos(cm.doc, op.scrollToPos), op.scrollToPosMargin); + } else if (newScrollPos) { + scrollCursorIntoView(cm); + } + if (op.selectionChanged) restartBlink(cm); + + if (cm.state.focused && op.updateInput) + resetInput(cm, op.userSelChange); + + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) for (var i = 0; i < hidden.length; ++i) + if (!hidden[i].lines.length) signal(hidden[i], "hide"); + if (unhidden) for (var i = 0; i < unhidden.length; ++i) + if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); + + var delayed; + if (!--delayedCallbackDepth) { + delayed = delayedCallbacks; + delayedCallbacks = null; + } + if (op.textChanged) + signal(cm, "change", cm, op.textChanged); + if (op.cursorActivity) signal(cm, "cursorActivity", cm); + if (delayed) for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm1, f) { + return function() { + var cm = cm1 || this, withOp = !cm.curOp; + if (withOp) startOperation(cm); + try { var result = f.apply(cm, arguments); } + finally { if (withOp) endOperation(cm); } + return result; + }; + } + function docOperation(f) { + return function() { + var withOp = this.cm && !this.cm.curOp, result; + if (withOp) startOperation(this.cm); + try { result = f.apply(this, arguments); } + finally { if (withOp) endOperation(this.cm); } + return result; + }; + } + function runInOp(cm, f) { + var withOp = !cm.curOp, result; + if (withOp) startOperation(cm); + try { result = f(); } + finally { if (withOp) endOperation(cm); } + return result; + } + + function regChange(cm, from, to, lendiff) { + if (from == null) from = cm.doc.first; + if (to == null) to = cm.doc.first + cm.doc.size; + cm.curOp.changes.push({from: from, to: to, diff: lendiff}); + } + + // INPUT HANDLING + + function slowPoll(cm) { + if (cm.display.pollingFast) return; + cm.display.poll.set(cm.options.pollInterval, function() { + readInput(cm); + if (cm.state.focused) slowPoll(cm); + }); + } + + function fastPoll(cm) { + var missed = false; + cm.display.pollingFast = true; + function p() { + var changed = readInput(cm); + if (!changed && !missed) {missed = true; cm.display.poll.set(60, p);} + else {cm.display.pollingFast = false; slowPoll(cm);} + } + cm.display.poll.set(20, p); + } + + // prevInput is a hack to work with IME. If we reset the textarea + // on every change, that breaks IME. So we look for changes + // compared to the previous content instead. (Modern browsers have + // events that indicate IME taking place, but these are not widely + // supported or compatible enough yet to rely on.) + function readInput(cm) { + var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel; + if (!cm.state.focused || hasSelection(input) || isReadOnly(cm)) return false; + var text = input.value; + if (text == prevInput && posEq(sel.from, sel.to)) return false; + if (ie && !ie_lt9 && cm.display.inputHasSelection === text) { + resetInput(cm, true); + return false; + } + + var withOp = !cm.curOp; + if (withOp) startOperation(cm); + sel.shift = false; + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; + var from = sel.from, to = sel.to; + if (same < prevInput.length) + from = Pos(from.line, from.ch - (prevInput.length - same)); + else if (cm.state.overwrite && posEq(from, to) && !cm.state.pasteIncoming) + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + (text.length - same))); + var updateInput = cm.curOp.updateInput; + makeChange(cm.doc, {from: from, to: to, text: splitLines(text.slice(same)), + origin: cm.state.pasteIncoming ? "paste" : "+input"}, "end"); + + cm.curOp.updateInput = updateInput; + if (text.length > 1000 || text.indexOf("\n") > -1) input.value = cm.display.prevInput = ""; + else cm.display.prevInput = text; + if (withOp) endOperation(cm); + cm.state.pasteIncoming = false; + return true; + } + + function resetInput(cm, user) { + var minimal, selected, doc = cm.doc; + if (!posEq(doc.sel.from, doc.sel.to)) { + cm.display.prevInput = ""; + minimal = hasCopyEvent && + (doc.sel.to.line - doc.sel.from.line > 100 || (selected = cm.getSelection()).length > 1000); + var content = minimal ? "-" : selected || cm.getSelection(); + cm.display.input.value = content; + if (cm.state.focused) selectInput(cm.display.input); + if (ie && !ie_lt9) cm.display.inputHasSelection = content; + } else if (user) { + cm.display.prevInput = cm.display.input.value = ""; + if (ie && !ie_lt9) cm.display.inputHasSelection = null; + } + cm.display.inaccurateSelection = minimal; + } + + function focusInput(cm) { + if (cm.options.readOnly != "nocursor" && (!mobile || document.activeElement != cm.display.input)) + cm.display.input.focus(); + } + + function isReadOnly(cm) { + return cm.options.readOnly || cm.doc.cantEdit; + } + + // EVENT HANDLERS + + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + if (ie) + on(d.scroller, "dblclick", operation(cm, function(e) { + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; + e_preventDefault(e); + var word = findWordAt(getLine(cm.doc, pos.line).text, pos); + extendSelection(cm.doc, word.from, word.to); + })); + else + on(d.scroller, "dblclick", e_preventDefault); + on(d.lineSpace, "selectstart", function(e) { + if (!eventInWidget(d, e)) e_preventDefault(e); + }); // 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(scroller, "contextmenu", onContextMenu); - connect(scroller, "scroll", onScrollMain); - connect(scrollbar, "scroll", onScrollBar); - connect(scrollbar, "mousedown", function() {if (focused) setTimeout(focusInput, 0);}); - var resizeHandler = connect(window, "resize", function() { - if (wrapper.parentNode) updateDisplay(true); - else resizeHandler(); - }, true); - connect(input, "keyup", operation(onKeyUp)); - connect(input, "input", fastPoll); - connect(input, "keydown", operation(onKeyDown)); - connect(input, "keypress", operation(onKeyPress)); - connect(input, "focus", onFocus); - connect(input, "blur", onBlur); + if (!captureMiddleClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + + on(d.scroller, "scroll", function() { + if (d.scroller.clientHeight) { + setScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + on(d.scrollbarV, "scroll", function() { + if (d.scroller.clientHeight) setScrollTop(cm, d.scrollbarV.scrollTop); + }); + on(d.scrollbarH, "scroll", function() { + if (d.scroller.clientHeight) setScrollLeft(cm, d.scrollbarH.scrollLeft); + }); + + on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); + on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + + function reFocus() { if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); } + on(d.scrollbarH, "mousedown", reFocus); + on(d.scrollbarV, "mousedown", reFocus); + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + function onResize() { + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = null; + clearCaches(cm); + runInOp(cm, bind(regChange, cm)); + } + on(window, "resize", onResize); + // Above handler holds on to the editor and its data structures. + // Here we poll to unregister it when the editor is no longer in + // the document, so that it can be garbage-collected. + function unregister() { + for (var p = d.wrapper.parentNode; p && p != document.body; p = p.parentNode) {} + if (p) setTimeout(unregister, 5000); + else off(window, "resize", onResize); + } + setTimeout(unregister, 5000); + + on(d.input, "keyup", operation(cm, function(e) { + if (cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + if (e.keyCode == 16) cm.doc.sel.shift = false; + })); + on(d.input, "input", bind(fastPoll, cm)); + on(d.input, "keydown", operation(cm, onKeyDown)); + on(d.input, "keypress", operation(cm, onKeyPress)); + on(d.input, "focus", bind(onFocus, cm)); + on(d.input, "blur", bind(onBlur, cm)); function drag_(e) { - if (options.onDragEvent && options.onDragEvent(instance, addStop(e))) return; + if (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))) return; e_stop(e); } - if (options.dragDrop) { - connect(scroller, "dragstart", onDragStart); - connect(scroller, "dragenter", drag_); - connect(scroller, "dragover", drag_); - connect(scroller, "drop", operation(onDrop)); - } - connect(scroller, "paste", function(){focusInput(); fastPoll();}); - connect(input, "paste", function(){pasteIncoming = true; fastPoll();}); - connect(input, "cut", operation(function(){ - if (!options.readOnly) replaceSelection(""); - })); + if (cm.options.dragDrop) { + on(d.scroller, "dragstart", function(e){onDragStart(cm, e);}); + on(d.scroller, "dragenter", drag_); + on(d.scroller, "dragover", drag_); + on(d.scroller, "drop", operation(cm, onDrop)); + } + on(d.scroller, "paste", function(e){ + if (eventInWidget(d, e)) return; + focusInput(cm); + fastPoll(cm); + }); + on(d.input, "paste", function() { + cm.state.pasteIncoming = true; + fastPoll(cm); + }); + + function prepareCopy() { + if (d.inaccurateSelection) { + d.prevInput = ""; + d.inaccurateSelection = false; + d.input.value = cm.getSelection(); + selectInput(d.input); + } + } + on(d.input, "cut", prepareCopy); + on(d.input, "copy", prepareCopy); // Needed to handle Tab key in KHTML - if (khtml) connect(sizer, "mouseup", function() { - if (document.activeElement == input) input.blur(); - focusInput(); + if (khtml) on(d.sizer, "mouseup", function() { + if (document.activeElement == d.input) d.input.blur(); + focusInput(cm); }); - - // IE throws unspecified error in certain cases, when - // trying to access activeElement before onload - var hasFocus; try { hasFocus = (document.activeElement == input); } catch(e) { } - if (hasFocus || options.autofocus) setTimeout(onFocus, 20); - else onBlur(); - - function isLine(l) {return l >= 0 && l < doc.size;} - // The instance object that we'll return. Mostly calls out to - // local functions in the CodeMirror function. Some do some extra - // 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 = wrapper.CodeMirror = { - getValue: getValue, - setValue: operation(setValue), - getSelection: getSelection, - replaceSelection: operation(replaceSelection), - focus: function(){window.focus(); focusInput(); onFocus(); fastPoll();}, - setOption: function(option, value) { - var oldVal = options[option]; - options[option] = value; - if (option == "mode" || option == "indentUnit") loadMode(); - else if (option == "readOnly" && value == "nocursor") {onBlur(); input.blur();} - else if (option == "readOnly" && !value) {resetInput(true);} - else if (option == "theme") themeChanged(); - else if (option == "lineWrapping" && oldVal != value) operation(wrappingChanged)(); - else if (option == "tabSize") updateDisplay(true); - else if (option == "keyMap") keyMapChanged(); - else if (option == "tabindex") input.tabIndex = value; - if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" || - option == "theme" || option == "lineNumberFormatter") { - gutterChanged(); - updateDisplay(true); - } - }, - getOption: function(option) {return options[option];}, - getMode: function() {return mode;}, - undo: operation(undo), - redo: operation(redo), - indentLine: operation(function(n, dir) { - if (typeof dir != "string") { - if (dir == null) dir = options.smartIndent ? "smart" : "prev"; - else dir = dir ? "add" : "subtract"; - } - if (isLine(n)) indentLine(n, dir); - }), - indentSelection: operation(indentSelected), - historySize: function() {return {undo: history.done.length, redo: history.undone.length};}, - clearHistory: function() {history = new History();}, - setHistory: function(histData) { - history = new History(); - history.done = histData.done; - history.undone = histData.undone; - }, - getHistory: function() { - function cp(arr) { - for (var i = 0, nw = [], nwelt; i < arr.length; ++i) { - nw.push(nwelt = []); - for (var j = 0, elt = arr[i]; j < elt.length; ++j) { - var old = [], cur = elt[j]; - nwelt.push({start: cur.start, added: cur.added, old: old}); - for (var k = 0; k < cur.old.length; ++k) old.push(hlText(cur.old[k])); - } - } - return nw; - } - return {done: cp(history.done), undone: cp(history.undone)}; - }, - matchBrackets: operation(function(){matchBrackets(true);}), - getTokenAt: operation(function(pos) { - pos = clipPos(pos); - return getLine(pos.line).getTokenAt(mode, getStateBefore(pos.line), options.tabSize, pos.ch); - }), - getStateAfter: function(line) { - line = clipLine(line == null ? doc.size - 1: line); - return getStateBefore(line + 1); - }, - cursorCoords: function(start, mode) { - if (start == null) start = sel.inverted; - return this.charCoords(start ? sel.from : sel.to, mode); - }, - charCoords: function(pos, mode) { - pos = clipPos(pos); - if (mode == "local") return localCoords(pos, false); - if (mode == "div") return localCoords(pos, true); - return pageCoords(pos); - }, - coordsChar: function(coords) { - var off = eltOffset(lineSpace); - return coordsChar(coords.x - off.left, coords.y - off.top); - }, - defaultTextHeight: function() { return textHeight(); }, - markText: operation(markText), - setBookmark: setBookmark, - findMarksAt: findMarksAt, - setMarker: operation(addGutterMarker), - clearMarker: operation(removeGutterMarker), - setLineClass: operation(setLineClass), - hideLine: operation(function(h) {return setLineHidden(h, true);}), - showLine: operation(function(h) {return setLineHidden(h, false);}), - onDeleteLine: function(line, f) { - if (typeof line == "number") { - if (!isLine(line)) return null; - line = getLine(line); - } - (line.handlers || (line.handlers = [])).push(f); - return line; - }, - lineInfo: lineInfo, - getViewport: function() { return {from: showingFrom, to: showingTo};}, - addWidget: function(pos, node, scroll, vert, horiz) { - pos = localCoords(clipPos(pos)); - var top = pos.yBot, left = pos.x; - node.style.position = "absolute"; - sizer.appendChild(node); - if (vert == "over") top = pos.y; - else if (vert == "near") { - var vspace = Math.max(scroller.offsetHeight, doc.height * textHeight()), - hspace = Math.max(sizer.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 = sizer.clientWidth - node.offsetWidth; - node.style.right = "0px"; - } else { - if (horiz == "left") left = 0; - else if (horiz == "middle") left = (sizer.clientWidth - node.offsetWidth) / 2; - node.style.left = (left + paddingLeft()) + "px"; + } + + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n) return true; + if (/\bCodeMirror-(?:line)?widget\b/.test(n.className) || + n.parentNode == display.sizer && n != display.mover) return true; + } + } + + function posFromMouse(cm, e, liberal) { + var display = cm.display; + if (!liberal) { + var target = e_target(e); + if (target == display.scrollbarH || target == display.scrollbarH.firstChild || + target == display.scrollbarV || target == display.scrollbarV.firstChild || + target == display.scrollbarFiller) return null; + } + var x, y, space = getRect(display.lineSpace); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX; y = e.clientY; } catch (e) { return null; } + return coordsChar(cm, x - space.left, y - space.top); + } + + var lastClick, lastDoubleClick; + function onMouseDown(e) { + var cm = this, display = cm.display, doc = cm.doc, sel = doc.sel; + sel.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + display.scroller.draggable = false; + setTimeout(function(){display.scroller.draggable = true;}, 100); + } + return; + } + if (clickInGutter(cm, e)) return; + var start = posFromMouse(cm, e); + + switch (e_button(e)) { + case 3: + if (captureMiddleClick) onContextMenu.call(cm, cm, e); + return; + case 2: + if (start) extendSelection(cm.doc, start); + setTimeout(bind(focusInput, cm), 20); + e_preventDefault(e); + return; + } + // For button 1, if it was clicked inside the editor + // (posFromMouse returning non-null), we have to adjust the + // selection. + if (!start) {if (e_target(e) == display.scroller) e_preventDefault(e); return;} + + if (!cm.state.focused) onFocus(cm); + + var now = +new Date, type = "single"; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) { + type = "triple"; + e_preventDefault(e); + setTimeout(bind(focusInput, cm), 20); + selectLine(cm, start.line); + } else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) { + type = "double"; + lastDoubleClick = {time: now, pos: start}; + e_preventDefault(e); + var word = findWordAt(getLine(doc, start.line).text, start); + extendSelection(cm.doc, word.from, word.to); + } else { lastClick = {time: now, pos: start}; } + + var last = start; + if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && !posEq(sel.from, sel.to) && + !posLess(start, sel.from) && !posLess(sel.to, start) && type == "single") { + var dragEnd = operation(cm, function(e2) { + if (webkit) display.scroller.draggable = false; + cm.state.draggingText = false; + off(document, "mouseup", dragEnd); + off(display.scroller, "drop", dragEnd); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + extendSelection(cm.doc, start); + focusInput(cm); } - if (scroll) - scrollIntoView(left, top, left + node.offsetWidth, top + node.offsetHeight); - }, - - lineCount: function() {return doc.size;}, - clipPos: clipPos, - getCursor: function(start) { - if (start == null) start = sel.inverted; - return copyPos(start ? sel.from : sel.to); - }, - somethingSelected: function() {return !posEq(sel.from, sel.to);}, - setCursor: operation(function(line, ch, user) { - if (ch == null && typeof line.line == "number") setCursor(line.line, line.ch, user); - else setCursor(line, ch, user); - }), - setSelection: operation(function(from, to, user) { - (user ? setSelectionUser : setSelection)(clipPos(from), clipPos(to || from)); - }), - getLine: function(line) {if (isLine(line)) return getLine(line).text;}, - getLineHandle: function(line) {if (isLine(line)) return getLine(line);}, - setLine: operation(function(line, text) { - if (isLine(line)) replaceRange(text, {line: line, ch: 0}, {line: line, ch: getLine(line).text.length}); - }), - removeLine: operation(function(line) { - if (isLine(line)) replaceRange("", {line: line, ch: 0}, clipPos({line: line+1, ch: 0})); - }), - replaceRange: operation(replaceRange), - getRange: function(from, to, lineSep) {return getRange(clipPos(from), clipPos(to), lineSep);}, - - triggerOnKeyDown: operation(onKeyDown), - execCommand: function(cmd) {return commands[cmd](instance);}, - // Stuff used by commands, probably not much use to outside code. - moveH: operation(moveH), - deleteH: operation(deleteH), - moveV: operation(moveV), - toggleOverwrite: function() { - if(overwrite){ - overwrite = false; - cursor.className = cursor.className.replace(" CodeMirror-overwrite", ""); - } else { - overwrite = true; - cursor.className += " CodeMirror-overwrite"; - } - }, - - posFromIndex: function(off) { - var lineNo = 0, ch; - doc.iter(0, doc.size, function(line) { - var sz = line.text.length + 1; - if (sz > off) { ch = off; return true; } - off -= sz; - ++lineNo; - }); - return clipPos({line: lineNo, ch: ch}); - }, - indexFromPos: function (coords) { - if (coords.line < 0 || coords.ch < 0) return 0; - var index = coords.ch; - doc.iter(0, coords.line, function (line) { - index += line.text.length + 1; - }); - return index; - }, - scrollTo: function(x, y) { - if (x != null) scroller.scrollLeft = x; - if (y != null) scrollbar.scrollTop = scroller.scrollTop = y; - updateDisplay([]); - }, - getScrollInfo: function() { - return {x: scroller.scrollLeft, y: scrollbar.scrollTop, - height: scrollbar.scrollHeight, width: scroller.scrollWidth}; - }, - scrollIntoView: function(pos) { - var coords = localCoords(pos ? clipPos(pos) : sel.inverted ? sel.from : sel.to); - scrollIntoView(coords.x, coords.y, coords.x, coords.yBot); - }, - - setSize: function(width, height) { - function interpret(val) { - val = String(val); - return /^\d+$/.test(val) ? val + "px" : val; - } - if (width != null) wrapper.style.width = interpret(width); - if (height != null) scroller.style.height = interpret(height); - instance.refresh(); - }, - - operation: function(f){return operation(f)();}, - compoundChange: function(f){return compoundChange(f);}, - refresh: function(){ - updateDisplay(true, null, lastScrollTop); - if (scrollbar.scrollHeight > lastScrollTop) - scrollbar.scrollTop = lastScrollTop; - }, - getInputField: function(){return input;}, - getWrapperElement: function(){return wrapper;}, - getScrollerElement: function(){return scroller;}, - getGutterElement: function(){return gutter;} - }; - - function getLine(n) { return getLineAt(doc, n); } - function updateLineHeight(line, height) { - gutterDirty = true; - var diff = height - line.height; - for (var n = line; n; n = n.parent) n.height += diff; - } - - function lineContent(line, wrapAt) { - if (!line.styles) - line.highlight(mode, line.stateAfter = getStateBefore(lineNo(line)), options.tabSize); - return line.getContent(options.tabSize, wrapAt, options.lineWrapping); - } - - function setValue(code) { - var top = {line: 0, ch: 0}; - updateLines(top, {line: doc.size - 1, ch: getLine(doc.size-1).text.length}, - splitLines(code), top, top); - updateInput = true; - } - function getValue(lineSep) { - var text = []; - doc.iter(0, doc.size, function(line) { text.push(line.text); }); - return text.join(lineSep || "\n"); - } - - function onScrollBar(e) { - if (Math.abs(scrollbar.scrollTop - lastScrollTop) > 1) { - lastScrollTop = scroller.scrollTop = scrollbar.scrollTop; - updateDisplay([]); + }); + // Let the drag handler handle this. + if (webkit) display.scroller.draggable = true; + cm.state.draggingText = dragEnd; + // IE's approach to draggable + if (display.scroller.dragDrop) display.scroller.dragDrop(); + on(document, "mouseup", dragEnd); + on(display.scroller, "drop", dragEnd); + return; + } + e_preventDefault(e); + if (type == "single") extendSelection(cm.doc, clipPos(doc, start)); + + var startstart = sel.from, startend = sel.to, lastPos = start; + + function doSelect(cur) { + if (posEq(lastPos, cur)) return; + lastPos = cur; + + if (type == "single") { + extendSelection(cm.doc, clipPos(doc, start), cur); + return; + } + + startstart = clipPos(doc, startstart); + startend = clipPos(doc, startend); + if (type == "double") { + var word = findWordAt(getLine(doc, cur.line).text, cur); + if (posLess(cur, startstart)) extendSelection(cm.doc, word.from, startend); + else extendSelection(cm.doc, startstart, word.to); + } else if (type == "triple") { + if (posLess(cur, startstart)) extendSelection(cm.doc, startend, clipPos(doc, Pos(cur.line, 0))); + else extendSelection(cm.doc, startstart, clipPos(doc, Pos(cur.line + 1, 0))); } } - function onScrollMain(e) { - if (options.fixedGutter && gutter.style.left != scroller.scrollLeft + "px") - gutter.style.left = scroller.scrollLeft + "px"; - if (Math.abs(scroller.scrollTop - lastScrollTop) > 1) { - lastScrollTop = scroller.scrollTop; - if (scrollbar.scrollTop != lastScrollTop) - scrollbar.scrollTop = lastScrollTop; - updateDisplay([]); + var editorSize = getRect(display.wrapper); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true); + if (!cur) return; + if (!posEq(cur, last)) { + if (!cm.state.focused) onFocus(cm); + last = cur; + doSelect(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) setTimeout(operation(cm, function() { + if (counter != curCount) return; + display.scroller.scrollTop += outside; + extend(e); + }), 50); } - if (options.onScroll) options.onScroll(instance); - } - - function onMouseDown(e) { - setShift(e_prop(e, "shiftKey")); - // Check whether this is a click in a widget - for (var n = e_target(e); n != wrapper; n = n.parentNode) - if (n.parentNode == sizer && n != mover) return; - - // See if this is a click in the gutter - 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, e); - return e_preventDefault(e); - } - - var start = posFromMouse(e); - - switch (e_button(e)) { - case 3: - if (gecko) onContextMenu(e); - return; - case 2: - if (start) setCursor(start.line, start.ch, true); - setTimeout(focusInput, 20); - e_preventDefault(e); + } + + function done(e) { + counter = Infinity; + var cur = posFromMouse(cm, e); + if (cur) doSelect(cur); + e_preventDefault(e); + focusInput(cm); + off(document, "mousemove", move); + off(document, "mouseup", up); + } + + var move = operation(cm, function(e) { + if (!ie && !e_button(e)) done(e); + else extend(e); + }); + var up = operation(cm, done); + on(document, "mousemove", move); + on(document, "mouseup", up); + } + + function onDrop(e) { + var cm = this; + if (eventInWidget(cm.display, e) || (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e)))) + return; + e_preventDefault(e); + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || isReadOnly(cm)) return; + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var loadFile = function(file, i) { + var reader = new FileReader; + reader.onload = function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(cm.doc, pos); + makeChange(cm.doc, {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}, "around"); + } + }; + reader.readAsText(file); + }; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } else { + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && !(posLess(pos, cm.doc.sel.from) || posLess(cm.doc.sel.to, pos))) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(bind(focusInput, cm), 20); return; } - // For button 1, if it was clicked inside the editor - // (posFromMouse returning non-null), we have to adjust the - // selection. - if (!start) {if (e_target(e) == scroller) e_preventDefault(e); return;} - - if (!focused) onFocus(); - - var now = +new Date, type = "single"; - if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) { - type = "triple"; - e_preventDefault(e); - setTimeout(focusInput, 20); - selectLine(start.line); - } else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) { - type = "double"; - lastDoubleClick = {time: now, pos: start}; - e_preventDefault(e); - var word = findWordAt(start); - setSelectionUser(word.from, word.to); - } else { lastClick = {time: now, pos: start}; } - - function dragEnd(e2) { - if (webkit) scroller.draggable = false; - draggingText = false; - up(); drop(); - if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { - e_preventDefault(e2); - setCursor(start.line, start.ch, true); - focusInput(); + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + var curFrom = cm.doc.sel.from, curTo = cm.doc.sel.to; + setSelection(cm.doc, pos, pos); + if (cm.state.draggingText) replaceRange(cm.doc, "", curFrom, curTo, "paste"); + cm.replaceSelection(text, null, "paste"); + focusInput(cm); + onFocus(cm); } } - var last = start, going; - if (options.dragDrop && dragAndDrop && !options.readOnly && !posEq(sel.from, sel.to) && - !posLess(start, sel.from) && !posLess(sel.to, start) && type == "single") { - // Let the drag handler handle this. - if (webkit) scroller.draggable = true; - var up = connect(document, "mouseup", operation(dragEnd), true); - var drop = connect(scroller, "drop", operation(dragEnd), true); - draggingText = true; - // IE's approach to draggable - if (scroller.dragDrop) scroller.dragDrop(); - return; + catch(e){} + } + } + + function clickInGutter(cm, e) { + var display = cm.display; + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + + if (mX >= Math.floor(getRect(display.gutters).right)) return false; + e_preventDefault(e); + if (!hasHandler(cm, "gutterClick")) return true; + + var lineBox = getRect(display.lineDiv); + if (mY > lineBox.bottom) return true; + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.options.gutters.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && getRect(g).right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.options.gutters[i]; + signalLater(cm, "gutterClick", cm, line, gutter, e); + break; + } + } + return true; + } + + function onDragStart(cm, e) { + if (ie && !cm.state.draggingText) { e_stop(e); return; } + if (eventInWidget(cm.display, e)) return; + + var txt = cm.getSelection(); + e.dataTransfer.setData("Text", txt); + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + if (opera) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + if (safari) { + if (cm.display.dragImg) { + img = cm.display.dragImg; + } else { + cm.display.dragImg = img; + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + cm.display.wrapper.appendChild(img); + } } - e_preventDefault(e); - if (type == "single") setCursor(start.line, start.ch, true); - - var startstart = sel.from, startend = sel.to; - - function doSelect(cur) { - if (type == "single") { - setSelectionUser(start, cur); - } else if (type == "double") { - var word = findWordAt(cur); - if (posLess(cur, startstart)) setSelectionUser(word.from, startend); - else setSelectionUser(startstart, word.to); - } else if (type == "triple") { - if (posLess(cur, startstart)) setSelectionUser(startend, clipPos({line: cur.line, ch: 0})); - else setSelectionUser(startstart, clipPos({line: cur.line + 1, ch: 0})); + e.dataTransfer.setDragImage(img, 0, 0); + if (opera) img.parentNode.removeChild(img); + } + } + + function setScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) return; + cm.doc.scrollTop = val; + if (!gecko) updateDisplay(cm, [], val); + if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; + if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val; + if (gecko) updateDisplay(cm, []); + } + function setScrollLeft(cm, val, isScroller) { + if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; + if (cm.display.scrollbarH.scrollLeft != val) cm.display.scrollbarH.scrollLeft = val; + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) wheelPixelsPerUnit = -.53; + else if (gecko) wheelPixelsPerUnit = 15; + else if (chrome) wheelPixelsPerUnit = -.7; + else if (safari) wheelPixelsPerUnit = -1/3; + + function onScrollWheel(cm, e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; + else if (dy == null) dy = e.wheelDelta; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + if (!(dx && scroll.scrollWidth > scroll.clientWidth || + dy && scroll.scrollHeight > scroll.clientHeight)) return; + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + for (var cur = e.target; cur != scroll; cur = cur.parentNode) { + if (cur.lineObj) { + cm.display.currentWheelTarget = cur; + break; } } - - function extend(e) { - var cur = posFromMouse(e, true); - if (cur && !posEq(cur, last)) { - if (!focused) onFocus(); - last = cur; - doSelect(cur); - updateInput = false; - var visible = visibleLines(); - if (cur.line >= visible.to || cur.line < visible.from) - going = setTimeout(operation(function(){extend(e);}), 150); + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !opera && wheelPixelsPerUnit != null) { + if (dy) + setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); + setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); + e_preventDefault(e); + display.wheelStartX = null; // Abort measurement, if in progress + return; + } + + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) top = Math.max(0, top + pixels - 50); + else bot = Math.min(cm.doc.height, bot + pixels + 50); + updateDisplay(cm, [], {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function() { + if (display.wheelStartX == null) return; + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) return; + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) return false; + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + if (cm.display.pollingFast && readInput(cm)) cm.display.pollingFast = false; + var doc = cm.doc, prevShift = doc.sel.shift, done = false; + try { + if (isReadOnly(cm)) cm.state.suppressEdits = true; + if (dropShift) doc.sel.shift = false; + done = bound(cm) != Pass; + } finally { + doc.sel.shift = prevShift; + cm.state.suppressEdits = false; + } + return done; + } + + function allKeyMaps(cm) { + var maps = cm.state.keyMaps.slice(0); + if (cm.options.extraKeys) maps.push(cm.options.extraKeys); + maps.push(cm.options.keyMap); + return maps; + } + + var maybeTransition; + function handleKeyBinding(cm, e) { + // Handle auto keymap transitions + var startMap = getKeyMap(cm.options.keyMap), next = startMap.auto; + clearTimeout(maybeTransition); + if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() { + if (getKeyMap(cm.options.keyMap) == startMap) + cm.options.keyMap = (next.call ? next.call(null, cm) : next); + }, 50); + + var name = keyName(e, true), handled = false; + if (!name) return false; + var keymaps = allKeyMaps(cm); + + if (e.shiftKey) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + handled = lookupKey("Shift-" + name, keymaps, function(b) {return doHandleBinding(cm, b, true);}) + || lookupKey(name, keymaps, function(b) { + if (typeof b == "string" && /^go[A-Z]/.test(b)) return doHandleBinding(cm, b); + }); + } else { + handled = lookupKey(name, keymaps, function(b) { return doHandleBinding(cm, b); }); + } + if (handled == "stop") handled = false; + + if (handled) { + e_preventDefault(e); + restartBlink(cm); + if (ie_lt9) { e.oldKeyCode = e.keyCode; e.keyCode = 0; } + } + return handled; + } + + function handleCharBinding(cm, e, ch) { + var handled = lookupKey("'" + ch + "'", allKeyMaps(cm), + function(b) { return doHandleBinding(cm, b, true); }); + if (handled) { + e_preventDefault(e); + restartBlink(cm); + } + return handled; + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + if (!cm.state.focused) onFocus(cm); + if (ie && e.keyCode == 27) { e.returnValue = false; } + if (cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + var code = e.keyCode; + // IE does strange things with escape. + cm.doc.sel.shift = code == 16 || e.shiftKey; + // First give onKeyEvent option a chance to handle this. + var handled = handleKeyBinding(cm, e); + if (opera) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + cm.replaceSelection(""); + } + } + + function onKeyPress(e) { + var cm = this; + if (cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + var keyCode = e.keyCode, charCode = e.charCode; + if (opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if (((opera && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return; + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + if (this.options.electricChars && this.doc.mode.electricChars && + this.options.smartIndent && !isReadOnly(this) && + this.doc.mode.electricChars.indexOf(ch) > -1) + setTimeout(operation(cm, function() {indentLine(cm, cm.doc.sel.to.line, "smart");}), 75); + if (handleCharBinding(cm, e, ch)) return; + if (ie && !ie_lt9) cm.display.inputHasSelection = null; + fastPoll(cm); + } + + function onFocus(cm) { + if (cm.options.readOnly == "nocursor") return; + if (!cm.state.focused) { + signal(cm, "focus", cm); + cm.state.focused = true; + if (cm.display.wrapper.className.search(/\bCodeMirror-focused\b/) == -1) + cm.display.wrapper.className += " CodeMirror-focused"; + resetInput(cm, true); + } + slowPoll(cm); + restartBlink(cm); + } + function onBlur(cm) { + if (cm.state.focused) { + signal(cm, "blur", cm); + cm.state.focused = false; + cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-focused", ""); + } + clearInterval(cm.display.blinker); + setTimeout(function() {if (!cm.state.focused) cm.doc.sel.shift = false;}, 150); + } + + var detectingSelectAll; + function onContextMenu(cm, e) { + var display = cm.display, sel = cm.doc.sel; + if (eventInWidget(display, e)) return; + + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || opera) return; // Opera is difficult. + if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to)) + operation(cm, setSelection)(cm.doc, pos, pos); + + var oldCSS = display.input.style.cssText; + display.inputDiv.style.position = "absolute"; + display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; outline: none;" + + "border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);"; + focusInput(cm); + resetInput(cm, true); + // Adds "Select all" to context menu in FF + if (posEq(sel.from, sel.to)) display.input.value = display.prevInput = " "; + + function rehide() { + display.inputDiv.style.position = "relative"; + display.input.style.cssText = oldCSS; + if (ie_lt9) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos; + slowPoll(cm); + + // Try to detect the user choosing select-all + if (display.input.selectionStart != null && (!ie || ie_lt9)) { + clearTimeout(detectingSelectAll); + var extval = display.input.value = " " + (posEq(sel.from, sel.to) ? "" : display.input.value), i = 0; + display.prevInput = " "; + display.input.selectionStart = 1; display.input.selectionEnd = extval.length; + var poll = function(){ + if (display.prevInput == " " && display.input.selectionStart == 0) + operation(cm, commands.selectAll)(cm); + else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500); + else resetInput(cm); + }; + detectingSelectAll = setTimeout(poll, 200); + } + } + + if (captureMiddleClick) { + e_stop(e); + var mouseup = function() { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + } + + // UPDATING + + function changeEnd(change) { + if (!change.text) return change.to; + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); + } + + // Make sure a position will be valid after the given change. + function clipPostChange(doc, change, pos) { + if (!posLess(change.from, pos)) return clipPos(doc, pos); + var diff = (change.text.length - 1) - (change.to.line - change.from.line); + if (pos.line > change.to.line + diff) { + var preLine = pos.line - diff, lastLine = doc.first + doc.size - 1; + if (preLine > lastLine) return Pos(lastLine, getLine(doc, lastLine).text.length); + return clipToLen(pos, getLine(doc, preLine).text.length); + } + if (pos.line == change.to.line + diff) + return clipToLen(pos, lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0) + + getLine(doc, change.to.line).text.length - change.to.ch); + var inside = pos.line - change.from.line; + return clipToLen(pos, change.text[inside].length + (inside ? 0 : change.from.ch)); + } + + // Hint can be null|"end"|"start"|"around"|{anchor,head} + function computeSelAfterChange(doc, change, hint) { + if (hint && typeof hint == "object") // Assumed to be {anchor, head} object + return {anchor: clipPostChange(doc, change, hint.anchor), + head: clipPostChange(doc, change, hint.head)}; + + if (hint == "start") return {anchor: change.from, head: change.from}; + + var end = changeEnd(change); + if (hint == "around") return {anchor: change.from, head: end}; + if (hint == "end") return {anchor: end, head: end}; + + // hint is null, leave the selection alone as much as possible + var adjustPos = function(pos) { + if (posLess(pos, change.from)) return pos; + if (!posLess(change.to, pos)) return end; + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += end.ch - change.to.ch; + return Pos(line, ch); + }; + return {anchor: adjustPos(doc.sel.anchor), head: adjustPos(doc.sel.head)}; + } + + function filterChange(doc, change) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + update: function(from, to, text, origin) { + if (from) this.from = clipPos(doc, from); + if (to) this.to = clipPos(doc, to); + if (text) this.text = text; + if (origin !== undefined) this.origin = origin; + }, + cancel: function() { this.canceled = true; } + }; + signal(doc, "beforeChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); + + if (obj.canceled) return null; + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; + } + + // Replace the range from from to to by the strings in replacement. + // change is a {from, to, text [, origin]} object + function makeChange(doc, change, selUpdate, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, selUpdate, ignoreReadOnly); + if (doc.cm.state.suppressEdits) return; + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change); + if (!change) return; + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 1; --i) + makeChangeNoReadonly(doc, {from: split[i].from, to: split[i].to, text: [""]}); + if (split.length) + makeChangeNoReadonly(doc, {from: split[0].from, to: split[0].to, text: change.text}, selUpdate); + } else { + makeChangeNoReadonly(doc, change, selUpdate); + } + } + + function makeChangeNoReadonly(doc, change, selUpdate) { + var selAfter = computeSelAfterChange(doc, change, selUpdate); + addToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + function makeChangeFromHistory(doc, type) { + if (doc.cm && doc.cm.state.suppressEdits) return; + + var hist = doc.history; + var event = (type == "undo" ? hist.done : hist.undone).pop(); + if (!event) return; + hist.dirtyCounter += type == "undo" ? -1 : 1; + + var anti = {changes: [], anchorBefore: event.anchorAfter, headBefore: event.headAfter, + anchorAfter: event.anchorBefore, headAfter: event.headBefore}; + (type == "undo" ? hist.undone : hist.done).push(anti); + + for (var i = event.changes.length - 1; i >= 0; --i) { + var change = event.changes[i]; + change.origin = type; + anti.changes.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change, null) + : {anchor: event.anchorBefore, head: event.headBefore}; + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + } + } + + function shiftDoc(doc, distance) { + function shiftPos(pos) {return Pos(pos.line + distance, pos.ch);} + doc.first += distance; + if (doc.cm) regChange(doc.cm, doc.first, doc.first, distance); + doc.sel.head = shiftPos(doc.sel.head); doc.sel.anchor = shiftPos(doc.sel.anchor); + doc.sel.from = shiftPos(doc.sel.from); doc.sel.to = shiftPos(doc.sel.to); + } + + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return; + } + if (change.from.line > doc.lastLine()) return; + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) selAfter = computeSelAfterChange(doc, change, null); + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans, selAfter); + else updateDoc(doc, change, spans, selAfter); + } + + function makeChangeSingleDocInEditor(cm, change, spans, selAfter) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(doc, getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function(line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true; + } + }); + } + + if (!posLess(doc.sel.head, change.from) && !posLess(change.to, doc.sel.head)) + cm.curOp.cursorActivity = true; + + updateDoc(doc, change, spans, selAfter, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function(line) { + var len = lineLength(doc, line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) cm.curOp.updateMaxLine = true; + } + + // Adjust frontier, schedule worker + doc.frontier = Math.min(doc.frontier, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + regChange(cm, from.line, to.line + 1, lendiff); + + if (hasHandler(cm, "change")) { + var changeObj = {from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin}; + if (cm.curOp.textChanged) { + for (var cur = cm.curOp.textChanged; cur.next; cur = cur.next) {} + cur.next = changeObj; + } else cm.curOp.textChanged = changeObj; + } + } + + function replaceRange(doc, code, from, to, origin) { + if (!to) to = from; + if (posLess(to, from)) { var tmp = to; to = from; from = tmp; } + if (typeof code == "string") code = splitLines(code); + makeChange(doc, {from: from, to: to, text: code, origin: origin}, null); + } + + // POSITION OBJECT + + function Pos(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + } + CodeMirror.Pos = Pos; + + function posEq(a, b) {return a.line == b.line && a.ch == b.ch;} + function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);} + function copyPos(x) {return Pos(x.line, x.ch);} + + // SELECTION + + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} + function clipPos(doc, pos) { + if (pos.line < doc.first) return Pos(doc.first, 0); + var last = doc.first + doc.size - 1; + if (pos.line > last) return Pos(last, getLine(doc, last).text.length); + return clipToLen(pos, getLine(doc, pos.line).text.length); + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) return Pos(pos.line, linelen); + else if (ch < 0) return Pos(pos.line, 0); + else return pos; + } + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} + + // If shift is held, this will move the selection anchor. Otherwise, + // it'll set the whole selection. + function extendSelection(doc, pos, other, bias) { + if (doc.sel.shift || doc.sel.extend) { + var anchor = doc.sel.anchor; + if (other) { + var posBefore = posLess(pos, anchor); + if (posBefore != posLess(other, anchor)) { + anchor = pos; + pos = other; + } else if (posBefore != posLess(pos, other)) { + pos = other; } } - - function done(e) { - clearTimeout(going); - var cur = posFromMouse(e); - if (cur) doSelect(cur); - e_preventDefault(e); - focusInput(); - updateInput = true; - move(); up(); - } - var move = connect(document, "mousemove", operation(function(e) { - clearTimeout(going); - e_preventDefault(e); - if (!ie && !e_button(e)) done(e); - else extend(e); - }), true); - var up = connect(document, "mouseup", operation(done), true); - } - function onDoubleClick(e) { - for (var n = e_target(e); n != wrapper; n = n.parentNode) - if (n.parentNode == gutterText) return e_preventDefault(e); - e_preventDefault(e); - } - function onDrop(e) { - if (options.onDragEvent && options.onDragEvent(instance, addStop(e))) return; - e_preventDefault(e); - 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; - var loadFile = function(file, i) { - var reader = new FileReader; - reader.onload = function() { - text[i] = reader.result; - if (++read == n) { - pos = clipPos(pos); - operation(function() { - var end = replaceRange(text.join(""), pos, pos); - setSelectionUser(pos, end); - })(); + setSelection(doc, anchor, pos, bias); + } else { + setSelection(doc, pos, other || pos, bias); + } + if (doc.cm) doc.cm.curOp.userSelChange = true; + } + + function filterSelectionChange(doc, anchor, head) { + var obj = {anchor: anchor, head: head}; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + obj.anchor = clipPos(doc, obj.anchor); obj.head = clipPos(doc, obj.head); + return obj; + } + + // Update the selection. Last two args are only used by + // updateDoc, since they have to be expressed in the line + // numbers before the update. + function setSelection(doc, anchor, head, bias, checkAtomic) { + if (!checkAtomic && hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) { + var filtered = filterSelectionChange(doc, anchor, head); + head = filtered.head; + anchor = filtered.anchor; + } + + var sel = doc.sel; + sel.goalColumn = null; + // Skip over atomic spans. + if (checkAtomic || !posEq(anchor, sel.anchor)) + anchor = skipAtomic(doc, anchor, bias, checkAtomic != "push"); + if (checkAtomic || !posEq(head, sel.head)) + head = skipAtomic(doc, head, bias, checkAtomic != "push"); + + if (posEq(sel.anchor, anchor) && posEq(sel.head, head)) return; + + sel.anchor = anchor; sel.head = head; + var inv = posLess(head, anchor); + sel.from = inv ? head : anchor; + sel.to = inv ? anchor : head; + + if (doc.cm) + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = + doc.cm.curOp.cursorActivity = true; + + signalLater(doc, "cursorActivity", doc); + } + + function reCheckSelection(cm) { + setSelection(cm.doc, cm.doc.sel.from, cm.doc.sel.to, null, "push"); + } + + function skipAtomic(doc, pos, bias, mayClear) { + var flipped = false, curPos = pos; + var dir = bias || 1; + doc.cantEdit = false; + search: for (;;) { + var line = getLine(doc, curPos.line); + if (line.markedSpans) { + for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } } - }; - reader.readAsText(file); - }; - for (var i = 0; i < n; ++i) loadFile(files[i], i); - } else { - // Don't do a replace if the drop happened inside of the selected text. - if (draggingText && !(posLess(pos, sel.from) || posLess(sel.to, pos))) return; - try { - var text = e.dataTransfer.getData("Text"); - if (text) { - compoundChange(function() { - var curFrom = sel.from, curTo = sel.to; - setSelectionUser(pos, pos); - if (draggingText) replaceRange("", curFrom, curTo); - replaceSelection(text); - focusInput(); - }); + if (!m.atomic) continue; + var newPos = m.find()[dir < 0 ? "from" : "to"]; + if (posEq(newPos, curPos)) { + newPos.ch += dir; + if (newPos.ch < 0) { + if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); + else newPos = null; + } else if (newPos.ch > line.text.length) { + if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); + else newPos = null; + } + if (!newPos) { + if (flipped) { + // Driven in a corner -- no valid cursor position found at all + // -- try again *with* clearing, if we didn't already + if (!mayClear) return skipAtomic(doc, pos, bias, true); + // Otherwise, turn off editing until further notice, and return the start of the doc + doc.cantEdit = true; + return Pos(doc.first, 0); + } + flipped = true; newPos = pos; dir = -dir; + } + } + curPos = newPos; + continue search; } } - catch(e){} - } - } - function onDragStart(e) { - var txt = getSelection(); - e.dataTransfer.setData("Text", txt); - - // Use dummy image instead of default browsers image. - if (e.dataTransfer.setDragImage) - e.dataTransfer.setDragImage(elt('img'), 0, 0); - } - - function doHandleBinding(bound, dropShift) { - if (typeof bound == "string") { - bound = commands[bound]; - if (!bound) return false; - } - var prevShift = shiftSelecting; - try { - if (options.readOnly) suppressEdits = true; - if (dropShift) shiftSelecting = null; - bound(instance); - } catch(e) { - if (e != Pass) throw e; - return false; - } finally { - shiftSelecting = prevShift; - suppressEdits = false; - } - return true; - } - var maybeTransition; - function handleKeyBinding(e) { - // Handle auto keymap transitions - var startMap = getKeyMap(options.keyMap), next = startMap.auto; - clearTimeout(maybeTransition); - if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() { - if (getKeyMap(options.keyMap) == startMap) { - options.keyMap = (next.call ? next.call(null, instance) : next); - } - }, 50); - - var name = keyNames[e_prop(e, "keyCode")], handled = false; - var flipCtrlCmd = opera && mac; - if (name == null || e.altGraphKey) return false; - if (e_prop(e, "altKey")) name = "Alt-" + name; - if (e_prop(e, flipCtrlCmd ? "metaKey" : "ctrlKey")) name = "Ctrl-" + name; - if (e_prop(e, flipCtrlCmd ? "ctrlKey" : "metaKey")) name = "Cmd-" + name; - - var stopped = false; - function stop() { stopped = true; } - - if (e_prop(e, "shiftKey")) { - handled = lookupKey("Shift-" + name, options.extraKeys, options.keyMap, - function(b) {return doHandleBinding(b, true);}, stop) - || lookupKey(name, options.extraKeys, options.keyMap, function(b) { - if (typeof b == "string" && /^go[A-Z]/.test(b)) return doHandleBinding(b); - }, stop); - } else { - handled = lookupKey(name, options.extraKeys, options.keyMap, doHandleBinding, stop); - } - if (stopped) handled = false; - if (handled) { - e_preventDefault(e); - restartBlink(); - if (ie_lt9) { e.oldKeyCode = e.keyCode; e.keyCode = 0; } - } - return handled; - } - function handleCharBinding(e, ch) { - var handled = lookupKey("'" + ch + "'", options.extraKeys, - options.keyMap, function(b) { return doHandleBinding(b, true); }); - if (handled) { - e_preventDefault(e); - restartBlink(); } - return handled; - } - - var lastStoppedKey = null; - function onKeyDown(e) { - if (!focused) onFocus(); - if (ie && e.keyCode == 27) { e.returnValue = false; } - if (pollingFast) { if (readInput()) pollingFast = false; } - if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; - var code = e_prop(e, "keyCode"); - // IE does strange things with escape. - setShift(code == 16 || e_prop(e, "shiftKey")); - // First give onKeyEvent option a chance to handle this. - var handled = handleKeyBinding(e); - if (opera) { - lastStoppedKey = handled ? code : null; - // Opera has no cut event... we try to at least catch the key combo - if (!handled && code == 88 && e_prop(e, mac ? "metaKey" : "ctrlKey")) - replaceSelection(""); - } - } - function onKeyPress(e) { - if (pollingFast) readInput(); - if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; - var keyCode = e_prop(e, "keyCode"), charCode = e_prop(e, "charCode"); - if (opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} - if (((opera && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(e)) return; - var ch = String.fromCharCode(charCode == null ? keyCode : charCode); - if (options.electricChars && mode.electricChars && options.smartIndent && !options.readOnly) { - if (mode.electricChars.indexOf(ch) > -1) - setTimeout(operation(function() {indentLine(sel.to.line, "smart");}), 75); + return curPos; + } + } + + // SCROLLING + + function scrollCursorIntoView(cm) { + var coords = scrollPosIntoView(cm, cm.doc.sel.head); + if (!cm.state.focused) return; + var display = cm.display, box = getRect(display.sizer), doScroll = null, pTop = paddingTop(cm.display); + if (coords.top + pTop + box.top < 0) doScroll = true; + else if (coords.bottom + pTop + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; + if (doScroll != null && !phantom) { + var hidden = display.cursor.style.display == "none"; + if (hidden) { + display.cursor.style.display = ""; + display.cursor.style.left = coords.left + "px"; + display.cursor.style.top = (coords.top - display.viewOffset) + "px"; } - if (handleCharBinding(e, ch)) return; - fastPoll(); - } - function onKeyUp(e) { - if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; - if (e_prop(e, "keyCode") == 16) shiftSelecting = null; - } - - function onFocus() { - if (options.readOnly == "nocursor") return; - if (!focused) { - if (options.onFocus) options.onFocus(instance); - focused = true; - if (scroller.className.search(/\bCodeMirror-focused\b/) == -1) - scroller.className += " CodeMirror-focused"; + display.cursor.scrollIntoView(doScroll); + if (hidden) display.cursor.style.display = "none"; + } + } + + function scrollPosIntoView(cm, pos, margin) { + if (margin == null) margin = 0; + for (;;) { + var changed = false, coords = cursorCoords(cm, pos); + var scrollPos = calculateScrollPos(cm, coords.left, coords.top - margin, coords.left, coords.bottom + margin); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + setScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; } - slowPoll(); - restartBlink(); - } - function onBlur() { - if (focused) { - if (options.onBlur) options.onBlur(instance); - focused = false; - if (bracketHighlighted) - operation(function(){ - if (bracketHighlighted) { bracketHighlighted(); bracketHighlighted = null; } - })(); - scroller.className = scroller.className.replace(" CodeMirror-focused", ""); - } - clearInterval(blinker); - setTimeout(function() {if (!focused) shiftSelecting = null;}, 150); - } - - // Replace the range from from to to by the strings in newText. - // Afterwards, set the selection to selFrom, selTo. - function updateLines(from, to, newText, selFrom, selTo) { - if (suppressEdits) return; - var old = []; - doc.iter(from.line, to.line + 1, function(line) { - old.push(newHL(line.text, line.markedSpans)); - }); - if (history) { - history.addChange(from.line, newText.length, old); - while (history.done.length > options.undoDepth) history.done.shift(); + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; } - var lines = updateMarkedSpans(hlSpans(old[0]), hlSpans(lst(old)), from.ch, to.ch, newText); - updateLinesNoUndo(from, to, lines, selFrom, selTo); - } - function unredoHelper(from, to) { - if (!from.length) return; - var set = from.pop(), out = []; - for (var i = set.length - 1; i >= 0; i -= 1) { - var change = set[i]; - var replaced = [], end = change.start + change.added; - doc.iter(change.start, end, function(line) { replaced.push(newHL(line.text, line.markedSpans)); }); - out.push({start: change.start, added: change.old.length, old: replaced}); - var pos = {line: change.start + change.old.length - 1, - ch: editEnd(hlText(lst(replaced)), hlText(lst(change.old)))}; - updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length}, - change.old, pos, pos); - } - updateInput = true; - to.push(out); - } - function undo() {unredoHelper(history.done, history.undone);} - function redo() {unredoHelper(history.undone, history.done);} - - function updateLinesNoUndo(from, to, lines, selFrom, selTo) { - if (suppressEdits) return; - var recomputeMaxLength = false, maxLineLength = maxLine.text.length; - if (!options.lineWrapping) - doc.iter(from.line, to.line + 1, function(line) { - if (!line.hidden && line.text.length == maxLineLength) {recomputeMaxLength = true; return true;} - }); - if (from.line != to.line || lines.length > 1) gutterDirty = true; - - var nlines = to.line - from.line, firstLine = getLine(from.line), lastLine = getLine(to.line); - var lastHL = lst(lines); - - // First adjust the line structure - if (from.ch == 0 && to.ch == 0 && hlText(lastHL) == "") { - // This is a whole-line replace. Treated specially to make - // sure line objects move the way they are supposed to. - var added = [], prevLine = null; - for (var i = 0, e = lines.length - 1; i < e; ++i) - added.push(new Line(hlText(lines[i]), hlSpans(lines[i]))); - lastLine.update(lastLine.text, hlSpans(lastHL)); - if (nlines) doc.remove(from.line, nlines, callbacks); - if (added.length) doc.insert(from.line, added); - } else if (firstLine == lastLine) { - if (lines.length == 1) { - firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]) + firstLine.text.slice(to.ch), hlSpans(lines[0])); - } else { - for (var added = [], i = 1, e = lines.length - 1; i < e; ++i) - added.push(new Line(hlText(lines[i]), hlSpans(lines[i]))); - added.push(new Line(hlText(lastHL) + firstLine.text.slice(to.ch), hlSpans(lastHL))); - firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]), hlSpans(lines[0])); - doc.insert(from.line + 1, added); - } - } else if (lines.length == 1) { - firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]) + lastLine.text.slice(to.ch), hlSpans(lines[0])); - doc.remove(from.line + 1, nlines, callbacks); - } else { - var added = []; - firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]), hlSpans(lines[0])); - lastLine.update(hlText(lastHL) + lastLine.text.slice(to.ch), hlSpans(lastHL)); - for (var i = 1, e = lines.length - 1; i < e; ++i) - added.push(new Line(hlText(lines[i]), hlSpans(lines[i]))); - if (nlines > 1) doc.remove(from.line + 1, nlines - 1, callbacks); - doc.insert(from.line + 1, added); - } - if (options.lineWrapping) { - var perLine = Math.max(5, scroller.clientWidth / charWidth() - 3); - doc.iter(from.line, from.line + lines.length, function(line) { - if (line.hidden) return; - var guess = Math.ceil(line.text.length / perLine) || 1; - if (guess != line.height) updateLineHeight(line, guess); - }); - } else { - doc.iter(from.line, from.line + lines.length, function(line) { - var l = line.text; - if (!line.hidden && l.length > maxLineLength) { - maxLine = line; maxLineLength = l.length; maxLineChanged = true; - recomputeMaxLength = false; - } - }); - if (recomputeMaxLength) updateMaxLine = true; - } - - // Adjust frontier, schedule worker - frontier = Math.min(frontier, from.line); - startWorker(400); - - var lendiff = lines.length - nlines - 1; - // Remember that these lines changed, for updating the display - changes.push({from: from.line, to: to.line + 1, diff: lendiff}); - if (options.onChange) { - // Normalize lines to contain only strings, since that's what - // the change event handler expects - for (var i = 0; i < lines.length; ++i) - if (typeof lines[i] != "string") lines[i] = lines[i].text; - var changeObj = {from: from, to: to, text: lines}; - if (textChanged) { - for (var cur = textChanged; cur.next; cur = cur.next) {} - cur.next = changeObj; - } else textChanged = changeObj; - } - - // Update the selection - function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;} - setSelection(clipPos(selFrom), clipPos(selTo), - updateLine(sel.from.line), updateLine(sel.to.line)); - } - - function needsScrollbar() { - var realHeight = doc.height * textHeight() + 2 * paddingTop(); - return realHeight * .99 > scroller.offsetHeight ? realHeight : false; - } - - function updateVerticalScroll(scrollTop) { - var scrollHeight = needsScrollbar(); - scrollbar.style.display = scrollHeight ? "block" : "none"; - if (scrollHeight) { - scrollbarInner.style.height = sizer.style.minHeight = scrollHeight + "px"; - scrollbar.style.height = scroller.clientHeight + "px"; - if (scrollTop != null) { - scrollbar.scrollTop = scroller.scrollTop = scrollTop; - // 'Nudge' the scrollbar to work around a Webkit bug where, - // in some situations, we'd end up with a scrollbar that - // reported its scrollTop (and looked) as expected, but - // *behaved* as if it was still in a previous state (i.e. - // couldn't scroll up, even though it appeared to be at the - // bottom). - if (webkit) setTimeout(function() { - if (scrollbar.scrollTop != scrollTop) return; - scrollbar.scrollTop = scrollTop + (scrollTop ? -1 : 1); - scrollbar.scrollTop = scrollTop; - }, 0); - } - } else { - sizer.style.minHeight = ""; - } - // Position the mover div to align with the current virtual scroll position - mover.style.top = displayOffset * textHeight() + "px"; - } - - function computeMaxLength() { - maxLine = getLine(0); maxLineChanged = true; - var maxLineLength = maxLine.text.length; - doc.iter(1, doc.size, function(line) { - var l = line.text; - if (!line.hidden && l.length > maxLineLength) { - maxLineLength = l.length; maxLine = line; - } - }); - updateMaxLine = false; - } - - function replaceRange(code, from, to) { - from = clipPos(from); - if (!to) to = from; else to = clipPos(to); - code = splitLines(code); - function adjustPos(pos) { - if (posLess(pos, from)) return pos; - if (!posLess(to, pos)) return end; - var line = pos.line + code.length - (to.line - from.line) - 1; - var ch = pos.ch; - if (pos.line == to.line) - ch += lst(code).length - (to.ch - (to.line == from.line ? from.ch : 0)); - return {line: line, ch: ch}; + if (!changed) return coords; + } + } + + function scrollIntoView(cm, x1, y1, x2, y2) { + var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); + if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); + if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); + } + + function calculateScrollPos(cm, x1, y1, x2, y2) { + var display = cm.display, pt = paddingTop(display); + y1 += pt; y2 += pt; + if (y1 < 0) y1 = 0; + var screen = display.scroller.clientHeight - scrollerCutOff, screentop = display.scroller.scrollTop, result = {}; + var docBottom = cm.doc.height + paddingVert(display); + var atTop = y1 < pt + 10, atBottom = y2 + pt > docBottom - 10; + if (y1 < screentop) { + result.scrollTop = atTop ? 0 : y1; + } else if (y2 > screentop + screen) { + var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); + if (newTop != screentop) result.scrollTop = newTop; + } + + var screenw = display.scroller.clientWidth - scrollerCutOff, screenleft = display.scroller.scrollLeft; + x1 += display.gutters.offsetWidth; x2 += display.gutters.offsetWidth; + var gutterw = display.gutters.offsetWidth; + var atLeft = x1 < gutterw + 10; + if (x1 < screenleft + gutterw || atLeft) { + if (atLeft) x1 = 0; + result.scrollLeft = Math.max(0, x1 - 10 - gutterw); + } else if (x2 > screenw + screenleft - 3) { + result.scrollLeft = x2 + 10 - screenw; + } + return result; + } + + function updateScrollPos(cm, left, top) { + cm.curOp.updateScrollPos = {scrollLeft: left == null ? cm.doc.scrollLeft : left, + scrollTop: top == null ? cm.doc.scrollTop : top}; + } + + function addToScrollPos(cm, left, top) { + var pos = cm.curOp.updateScrollPos || (cm.curOp.updateScrollPos = {scrollLeft: cm.doc.scrollLeft, scrollTop: cm.doc.scrollTop}); + var scroll = cm.display.scroller; + pos.scrollTop = Math.max(0, Math.min(scroll.scrollHeight - scroll.clientHeight, pos.scrollTop + top)); + pos.scrollLeft = Math.max(0, Math.min(scroll.scrollWidth - scroll.clientWidth, pos.scrollLeft + left)); + } + + // API UTILITIES + + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc; + if (!how) how = "add"; + if (how == "smart") { + if (!cm.doc.mode.indent) how = "prev"; + else var state = getStateBefore(cm, n); + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (how == "smart") { + indentation = cm.doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass) { + if (!aggressive) return; + how = "prev"; } - var end; - replaceRange1(code, from, to, function(end1) { - end = end1; - return {from: adjustPos(sel.from), to: adjustPos(sel.to)}; - }); - return end; - } - function replaceSelection(code, collapse) { - replaceRange1(splitLines(code), sel.from, sel.to, function(end) { - if (collapse == "end") return {from: end, to: end}; - else if (collapse == "start") return {from: sel.from, to: sel.from}; - else return {from: sel.from, to: end}; - }); - } - function replaceRange1(code, from, to, computeSel) { - var endch = code.length == 1 ? code[0].length + from.ch : lst(code).length; - var newSel = computeSel({line: from.line + code.length - 1, ch: endch}); - updateLines(from, to, code, newSel.from, newSel.to); - } - - function getRange(from, to, lineSep) { - var l1 = from.line, l2 = to.line; - if (l1 == l2) return getLine(l1).text.slice(from.ch, to.ch); - var code = [getLine(l1).text.slice(from.ch)]; - doc.iter(l1 + 1, l2, function(line) { code.push(line.text); }); - code.push(getLine(l2).text.slice(0, to.ch)); - return code.join(lineSep || "\n"); - } - function getSelection(lineSep) { - return getRange(sel.from, sel.to, lineSep); - } - - function slowPoll() { - if (pollingFast) return; - poll.set(options.pollInterval, function() { - readInput(); - if (focused) slowPoll(); - }); - } - function fastPoll() { - var missed = false; - pollingFast = true; - function p() { - var changed = readInput(); - if (!changed && !missed) {missed = true; poll.set(60, p);} - else {pollingFast = false; slowPoll();} - } - poll.set(20, p); - } - - // Previnput is a hack to work with IME. If we reset the textarea - // on every change, that breaks IME. So we look for changes - // compared to the previous content instead. (Modern browsers have - // events that indicate IME taking place, but these are not widely - // supported or compatible enough yet to rely on.) - var prevInput = ""; - function readInput() { - if (!focused || hasSelection(input) || options.readOnly) return false; - var text = input.value; - if (text == prevInput) return false; - if (!nestedOperation) startOperation(); - shiftSelecting = null; - var same = 0, l = Math.min(prevInput.length, text.length); - while (same < l && prevInput[same] == text[same]) ++same; - if (same < prevInput.length) - sel.from = {line: sel.from.line, ch: sel.from.ch - (prevInput.length - same)}; - else if (overwrite && posEq(sel.from, sel.to) && !pasteIncoming) - sel.to = {line: sel.to.line, ch: Math.min(getLine(sel.to.line).text.length, sel.to.ch + (text.length - same))}; - replaceSelection(text.slice(same), "end"); - if (text.length > 1000) { input.value = prevInput = ""; } - else prevInput = text; - if (!nestedOperation) endOperation(); - pasteIncoming = false; + } + if (how == "prev") { + if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); + else indentation = 0; + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} + if (pos < indentation) indentString += spaceStr(indentation - pos); + + if (indentString != curSpaceString) + replaceRange(cm.doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + line.stateAfter = null; + } + + function changeLine(cm, handle, op) { + var no = handle, line = handle, doc = cm.doc; + if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no)) regChange(cm, no, no + 1); + else return null; + return line; + } + + function findPosH(doc, pos, dir, unit, visually) { + var line = pos.line, ch = pos.ch; + var lineObj = getLine(doc, line); + var possible = true; + function findNextLine() { + var l = line + dir; + if (l < doc.first || l >= doc.first + doc.size) return (possible = false); + line = l; + return lineObj = getLine(doc, l); + } + function moveOnce(boundToLine) { + var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); + if (next == null) { + if (!boundToLine && findNextLine()) { + if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); + else ch = dir < 0 ? lineObj.text.length : 0; + } else return (possible = false); + } else ch = next; return true; } - function resetInput(user) { - if (!posEq(sel.from, sel.to)) { - prevInput = ""; - input.value = getSelection(); - if (focused) selectInput(input); - } else if (user) prevInput = input.value = ""; - } - - function focusInput() { - if (options.readOnly != "nocursor") input.focus(); - } - - function scrollCursorIntoView() { - var coords = calculateCursorCoords(); - scrollIntoView(coords.x, coords.y, coords.x, coords.yBot); - if (!focused) return; - var box = sizer.getBoundingClientRect(), doScroll = null; - if (coords.y + box.top < 0) doScroll = true; - else if (coords.y + box.top + textHeight() > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; - if (doScroll != null) { - var hidden = cursor.style.display == "none"; - if (hidden) { - cursor.style.display = ""; - cursor.style.left = coords.x + "px"; - cursor.style.top = (coords.y - displayOffset) + "px"; + + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) break; + var cur = lineObj.text.charAt(ch) || "\n"; + var type = isWordChar(cur) ? "w" + : !group ? null + : /\s/.test(cur) ? null + : "p"; + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce();} + break; } - cursor.scrollIntoView(doScroll); - if (hidden) cursor.style.display = "none"; - } - } - function calculateCursorCoords() { - var cursor = localCoords(sel.inverted ? sel.from : sel.to); - var x = options.lineWrapping ? Math.min(cursor.x, lineSpace.offsetWidth) : cursor.x; - return {x: x, y: cursor.y, yBot: cursor.yBot}; - } - function scrollIntoView(x1, y1, x2, y2) { - var scrollPos = calculateScrollPos(x1, y1, x2, y2); - if (scrollPos.scrollLeft != null) {scroller.scrollLeft = scrollPos.scrollLeft;} - if (scrollPos.scrollTop != null) {scrollbar.scrollTop = scroller.scrollTop = scrollPos.scrollTop;} - } - function calculateScrollPos(x1, y1, x2, y2) { - var pl = paddingLeft(), pt = paddingTop(); - y1 += pt; y2 += pt; x1 += pl; x2 += pl; - var screen = scroller.clientHeight, screentop = scrollbar.scrollTop, result = {}; - var docBottom = needsScrollbar() || Infinity; - var atTop = y1 < pt + 10, atBottom = y2 + pt > docBottom - 10; - if (y1 < screentop) result.scrollTop = atTop ? 0 : Math.max(0, y1); - else if (y2 > screentop + screen) result.scrollTop = (atBottom ? docBottom : y2) - screen; - - var screenw = scroller.clientWidth, screenleft = scroller.scrollLeft; - var gutterw = options.fixedGutter ? gutter.clientWidth : 0; - var atLeft = x1 < gutterw + pl + 10; - if (x1 < screenleft + gutterw || atLeft) { - if (atLeft) x1 = 0; - result.scrollLeft = Math.max(0, x1 - 10 - gutterw); - } else if (x2 > screenw + screenleft - 3) { - result.scrollLeft = x2 + 10 - screenw; - } - return result; - } - - function visibleLines(scrollTop) { - var lh = textHeight(), top = (scrollTop != null ? scrollTop : scrollbar.scrollTop) - paddingTop(); - var fromHeight = Math.max(0, Math.floor(top / lh)); - var toHeight = Math.ceil((top + scroller.clientHeight) / lh); - return {from: lineAtHeight(doc, fromHeight), - to: lineAtHeight(doc, toHeight)}; - } - // 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, suppressCallback, scrollTop) { - if (!scroller.clientWidth) { - showingFrom = showingTo = displayOffset = 0; - return; - } - // Compute the new visible window - // If scrollTop is specified, use that to determine which lines - // to render instead of the current scrollbar position. - var visible = visibleLines(scrollTop); - // Bail out if the visible area is already rendered and nothing changed. - if (changes !== true && changes.length == 0 && visible.from > showingFrom && visible.to < showingTo) { - updateVerticalScroll(scrollTop); - return; + if (type) sawType = type; + if (dir > 0 && !moveOnce(!first)) break; } - var from = Math.max(visible.from - 100, 0), to = Math.min(doc.size, visible.to + 100); - if (showingFrom < from && from - showingFrom < 20) from = showingFrom; - if (showingTo > to && showingTo - to < 20) to = Math.min(doc.size, showingTo); - - // Create a range of theoretically intact lines, and punch holes - // in that using the change info. - var intact = changes === true ? [] : - computeIntact([{from: showingFrom, to: showingTo, domStart: 0}], changes); - // Clip off the parts that won't be visible - var intactLines = 0; - for (var i = 0; i < intact.length; ++i) { - var range = intact[i]; - if (range.from < from) {range.domStart += (from - range.from); range.from = from;} - if (range.to > to) range.to = to; - if (range.from >= range.to) intact.splice(i--, 1); - else intactLines += range.to - range.from; - } - if (intactLines == to - from && from == showingFrom && to == showingTo) { - updateVerticalScroll(scrollTop); - return; - } - intact.sort(function(a, b) {return a.domStart - b.domStart;}); - - var th = textHeight(), gutterDisplay = gutter.style.display; - lineDiv.style.display = "none"; - patchDisplay(from, to, intact); - lineDiv.style.display = gutter.style.display = ""; - - var different = from != showingFrom || to != showingTo || lastSizeC != scroller.clientHeight + th; - // This is just a bogus formula that detects when the editor is - // resized or the font size changes. - if (different) lastSizeC = scroller.clientHeight + th; - if (from != showingFrom || to != showingTo && options.onViewportChange) - setTimeout(function(){ - if (options.onViewportChange) options.onViewportChange(instance, from, to); - }); - showingFrom = from; showingTo = to; - displayOffset = heightAtLine(doc, from); - startWorker(100); - - // Since this is all rather error prone, it is honoured with the - // only assertion in the whole file. - if (lineDiv.childNodes.length != showingTo - showingFrom) - throw new Error("BAD PATCH! " + JSON.stringify(intact) + " size=" + (showingTo - showingFrom) + - " nodes=" + lineDiv.childNodes.length); - - function checkHeights() { - var curNode = lineDiv.firstChild, heightChanged = false; - doc.iter(showingFrom, showingTo, function(line) { - // Work around bizarro IE7 bug where, sometimes, our curNode - // is magically replaced with a new node in the DOM, leaving - // us with a reference to an orphan (nextSibling-less) node. - if (!curNode) return; - if (!line.hidden) { - var height = Math.round(curNode.offsetHeight / th) || 1; - if (line.height != height) { - updateLineHeight(line, height); - gutterDirty = heightChanged = true; - } - } - curNode = curNode.nextSibling; - }); - return heightChanged; - } - - if (options.lineWrapping) checkHeights(); - - gutter.style.display = gutterDisplay; - if (different || gutterDirty) { - // If the gutter grew in size, re-check heights. If those changed, re-draw gutter. - updateGutter() && options.lineWrapping && checkHeights() && updateGutter(); + } + var result = skipAtomic(doc, Pos(line, ch), dir, true); + if (!possible) result.hitSide = true; + return result; + } + + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + for (;;) { + var target = coordsChar(cm, x, y); + if (!target.outside) break; + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } + y += dir * 5; + } + return target; + } + + function findWordAt(line, pos) { + var start = pos.ch, end = pos.ch; + if (line) { + if (pos.after === false || end == line.length) --start; else ++end; + var startChar = line.charAt(start); + var check = isWordChar(startChar) ? isWordChar + : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} + : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + } + return {from: Pos(pos.line, start), to: Pos(pos.line, end)}; + } + + function selectLine(cm, line) { + extendSelection(cm.doc, Pos(line, 0), clipPos(cm.doc, Pos(line + 1, 0))); + } + + // PROTOTYPE + + // The publicly visible API. Note that operation(null, f) means + // 'wrap f in an operation, performed on its `this` parameter' + + CodeMirror.prototype = { + focus: function(){window.focus(); focusInput(this); onFocus(this); fastPoll(this);}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") return; + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + operation(this, optionHandlers[option])(this, value, old); + }, + + getOption: function(option) {return this.options[option];}, + getDoc: function() {return this.doc;}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](map); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + if ((typeof map == "string" ? maps[i].name : maps[i]) == map) { + maps.splice(i, 1); + return true; + } + }, + + addOverlay: operation(null, function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) throw new Error("Overlays may not be stateful."); + this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: operation(null, function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + if (overlays[i].modeSpec == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return; + } } - updateVerticalScroll(scrollTop); - updateSelection(); - if (!suppressCallback && options.onUpdate) options.onUpdate(instance); - return true; - } - - function computeIntact(intact, changes) { - for (var i = 0, l = changes.length || 0; i < l; ++i) { - var change = changes[i], intact2 = [], diff = change.diff || 0; - for (var j = 0, l2 = intact.length; j < l2; ++j) { - var range = intact[j]; - if (change.to <= range.from && change.diff) - intact2.push({from: range.from + diff, to: range.to + diff, - domStart: range.domStart}); - else if (change.to <= range.from || change.from >= range.to) - intact2.push(range); - else { - if (change.from > range.from) - 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)}); - } - } - intact = intact2; + }), + + indentLine: operation(null, function(n, dir, aggressive) { + if (typeof dir != "string") { + if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; + else dir = dir ? "add" : "subtract"; } - return intact; - } - - function patchDisplay(from, to, intact) { - function killNode(node) { - var tmp = node.nextSibling; - node.parentNode.removeChild(node); - return tmp; - } - // The first pass removes the DOM nodes that aren't intact. - if (!intact.length) removeChildren(lineDiv); - else { - var domPos = 0, curNode = lineDiv.firstChild, n; - for (var i = 0; i < intact.length; ++i) { - var cur = intact[i]; - while (cur.domStart > domPos) {curNode = killNode(curNode); domPos++;} - for (var j = 0, e = cur.to - cur.from; j < e; ++j) {curNode = curNode.nextSibling; domPos++;} - } - while (curNode) curNode = killNode(curNode); + if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); + }), + indentSelection: operation(null, function(how) { + var sel = this.doc.sel; + if (posEq(sel.from, sel.to)) return indentLine(this, sel.from.line, how); + var e = sel.to.line - (sel.to.ch ? 0 : 1); + for (var i = sel.from.line; i <= e; ++i) indentLine(this, i, how); + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos) { + var doc = this.doc; + pos = clipPos(doc, pos); + var state = getStateBefore(this, pos.line), mode = this.doc.mode; + var line = getLine(doc, pos.line); + var stream = new StringStream(line.text, this.options.tabSize); + while (stream.pos < pos.ch && !stream.eol()) { + stream.start = stream.pos; + var style = mode.token(stream, state); } - // This pass fills in the lines that actually changed. - var nextIntact = intact.shift(), curNode = lineDiv.firstChild, j = from; - doc.iter(from, to, function(line) { - if (nextIntact && nextIntact.to == j) nextIntact = intact.shift(); - if (!nextIntact || nextIntact.from > j) { - if (line.hidden) var lineElement = elt("pre"); - else { - var lineElement = lineContent(line); - if (line.className) lineElement.className = line.className; - // Kludge to make sure the styled element lies behind the selection (by z-index) - if (line.bgClassName) { - var pre = elt("pre", "\u00a0", line.bgClassName, "position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: -2"); - lineElement = elt("div", [pre, lineElement], null, "position: relative"); - } - } - lineDiv.insertBefore(lineElement, curNode); - } else { - curNode = curNode.nextSibling; - } - ++j; + return {start: stream.start, + end: stream.pos, + string: stream.current(), + className: style || null, // Deprecated, use 'type' instead + type: style || null, + state: state}; + }, + + getStateAfter: function(line) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getStateBefore(this, line + 1); + }, + + cursorCoords: function(start, mode) { + var pos, sel = this.doc.sel; + if (start == null) pos = sel.head; + else if (typeof start == "object") pos = clipPos(this.doc, start); + else pos = start ? sel.from : sel.to; + return cursorCoords(this, pos, mode || "page"); + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page"); + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top); + }, + + defaultTextHeight: function() { return textHeight(this.display); }, + defaultCharWidth: function() { return charWidth(this.display); }, + + setGutterMarker: operation(null, function(line, gutterID, value) { + return changeLine(this, line, function(line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) line.gutterMarkers = null; + return true; }); - } - - function updateGutter() { - if (!options.gutter && !options.lineNumbers) return; - var hText = mover.offsetHeight, hEditor = scroller.clientHeight; - gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px"; - var fragment = document.createDocumentFragment(), i = showingFrom, normalNode; - doc.iter(showingFrom, Math.max(showingTo, showingFrom + 1), function(line) { - if (line.hidden) { - fragment.appendChild(elt("pre")); - } else { - var marker = line.gutterMarker; - var text = options.lineNumbers ? options.lineNumberFormatter(i + options.firstLineNumber) : null; - if (marker && marker.text) - text = marker.text.replace("%N%", text != null ? text : ""); - else if (text == null) - text = "\u00a0"; - var markerElement = fragment.appendChild(elt("pre", null, marker && marker.style)); - markerElement.innerHTML = text; - for (var j = 1; j < line.height; ++j) { - markerElement.appendChild(elt("br")); - markerElement.appendChild(document.createTextNode("\u00a0")); - } - if (!marker) normalNode = i; + }), + + clearGutter: operation(null, function(gutterID) { + var cm = this, doc = cm.doc, i = doc.first; + doc.iter(function(line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + line.gutterMarkers[gutterID] = null; + regChange(cm, i, i + 1); + if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; } ++i; }); - gutter.style.display = "none"; - removeChildrenAndAdd(gutterText, fragment); - // Make sure scrolling doesn't cause number gutter size to pop - if (normalNode != null && options.lineNumbers) { - var node = gutterText.childNodes[normalNode - showingFrom]; - var minwidth = String(doc.size).length, val = eltText(node.firstChild), pad = ""; - while (val.length + pad.length < minwidth) pad += "\u00a0"; - if (pad) node.insertBefore(document.createTextNode(pad), node.firstChild); - } - gutter.style.display = ""; - var resized = Math.abs((parseInt(lineSpace.style.marginLeft) || 0) - gutter.offsetWidth) > 2; - lineSpace.style.marginLeft = gutter.offsetWidth + "px"; - gutterDirty = false; - return resized; - } - function updateSelection() { - var collapsed = posEq(sel.from, sel.to); - var fromPos = localCoords(sel.from, true); - var toPos = collapsed ? fromPos : localCoords(sel.to, true); - var headPos = sel.inverted ? fromPos : toPos, th = textHeight(); - var wrapOff = eltOffset(wrapper), lineOff = eltOffset(lineDiv); - inputDiv.style.top = Math.max(0, Math.min(scroller.offsetHeight, headPos.y + lineOff.top - wrapOff.top)) + "px"; - inputDiv.style.left = Math.max(0, Math.min(scroller.offsetWidth, headPos.x + lineOff.left - wrapOff.left)) + "px"; - if (collapsed) { - cursor.style.top = headPos.y + "px"; - cursor.style.left = (options.lineWrapping ? Math.min(headPos.x, lineSpace.offsetWidth) : headPos.x) + "px"; - cursor.style.display = ""; - selectionDiv.style.display = "none"; - } else { - var sameLine = fromPos.y == toPos.y, fragment = document.createDocumentFragment(); - var clientWidth = lineSpace.clientWidth || lineSpace.offsetWidth; - var clientHeight = lineSpace.clientHeight || lineSpace.offsetHeight; - var add = function(left, top, right, height) { - var rstyle = quirksMode ? "width: " + (!right ? clientWidth : clientWidth - right - left) + "px" - : "right: " + right + "px"; - fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + - "px; top: " + top + "px; " + rstyle + "; height: " + height + "px")); - }; - if (sel.from.ch && fromPos.y >= 0) { - var right = sameLine ? clientWidth - toPos.x : 0; - add(fromPos.x, fromPos.y, right, th); - } - var middleStart = Math.max(0, fromPos.y + (sel.from.ch ? th : 0)); - var middleHeight = Math.min(toPos.y, clientHeight) - middleStart; - if (middleHeight > 0.2 * th) - add(0, middleStart, 0, middleHeight); - if ((!sameLine || !sel.from.ch) && toPos.y < clientHeight - .5 * th) - add(0, toPos.y, clientWidth - toPos.x, th); - removeChildrenAndAdd(selectionDiv, fragment); - cursor.style.display = "none"; - selectionDiv.style.display = ""; - } - } - - function setShift(val) { - if (val) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from); - else shiftSelecting = null; - } - 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); - userSelChange = true; - } - // 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) { - goalColumn = null; - if (oldFrom == null) {oldFrom = sel.from.line; oldTo = sel.to.line;} - if (posEq(sel.from, from) && posEq(sel.to, to)) return; - if (posLess(to, from)) {var tmp = to; to = from; from = tmp;} - - // Skip over hidden lines. - if (from.line != oldFrom) { - var from1 = skipHidden(from, oldFrom, sel.from.ch); - // If there is no non-hidden line left, force visibility on current line - if (!from1) setLineHidden(from.line, false); - else from = from1; - } - if (to.line != oldTo) to = skipHidden(to, oldTo, sel.to.ch); - - if (posEq(from, to)) sel.inverted = false; - else if (posEq(from, sel.to)) sel.inverted = false; - else if (posEq(to, sel.from)) sel.inverted = true; - - if (options.autoClearEmptyLines && posEq(sel.from, sel.to)) { - var head = sel.inverted ? from : to; - if (head.line != sel.from.line && sel.from.line < doc.size) { - var oldLine = getLine(sel.from.line); - if (/^\s+$/.test(oldLine.text)) - setTimeout(operation(function() { - if (oldLine.parent && /^\s+$/.test(oldLine.text)) { - var no = lineNo(oldLine); - replaceRange("", {line: no, ch: 0}, {line: no, ch: oldLine.text.length}); - } - }, 10)); - } - } - - sel.from = from; sel.to = to; - selectionChanged = true; - } - function skipHidden(pos, oldLine, oldCh) { - function getNonHidden(dir) { - var lNo = pos.line + dir, end = dir == 1 ? doc.size : -1; - while (lNo != end) { - var line = getLine(lNo); - if (!line.hidden) { - var ch = pos.ch; - if (toEnd || ch > oldCh || ch > line.text.length) ch = line.text.length; - return {line: lNo, ch: ch}; - } - lNo += dir; - } - } - var line = getLine(pos.line); - var toEnd = pos.ch == line.text.length && pos.ch != oldCh; - if (!line.hidden) return pos; - if (pos.line >= oldLine) return getNonHidden(1) || getNonHidden(-1); - else return getNonHidden(-1) || getNonHidden(1); - } - function setCursor(line, ch, user) { - var pos = clipPos({line: line, ch: ch || 0}); - (user ? setSelectionUser : setSelection)(pos, pos); - } - - function clipLine(n) {return Math.max(0, Math.min(n, doc.size-1));} - function clipPos(pos) { - if (pos.line < 0) return {line: 0, ch: 0}; - if (pos.line >= doc.size) return {line: doc.size-1, ch: getLine(doc.size-1).text.length}; - var ch = pos.ch, linelen = getLine(pos.line).text.length; - if (ch == null || ch > linelen) return {line: pos.line, ch: linelen}; - else if (ch < 0) return {line: pos.line, ch: 0}; - else return pos; - } - - function findPosH(dir, unit) { - var end = sel.inverted ? sel.from : sel.to, line = end.line, ch = end.ch; - var lineObj = getLine(line); - function findNextLine() { - for (var l = line + dir, e = dir < 0 ? -1 : doc.size; l != e; l += dir) { - var lo = getLine(l); - if (!lo.hidden) { line = l; lineObj = lo; return true; } - } - } - function moveOnce(boundToLine) { - if (ch == (dir < 0 ? 0 : lineObj.text.length)) { - if (!boundToLine && findNextLine()) ch = dir < 0 ? lineObj.text.length : 0; - else return false; - } else ch += dir; + }), + + addLineClass: operation(null, function(handle, where, cls) { + return changeLine(this, handle, function(line) { + var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass"; + if (!line[prop]) line[prop] = cls; + else if (new RegExp("\\b" + cls + "\\b").test(line[prop])) return false; + else line[prop] += " " + cls; return true; - } - if (unit == "char") moveOnce(); - else if (unit == "column") moveOnce(true); - else if (unit == "word") { - var sawWord = false; - for (;;) { - if (dir < 0) if (!moveOnce()) break; - if (isWordChar(lineObj.text.charAt(ch))) sawWord = true; - else if (sawWord) {if (dir < 0) {dir = 1; moveOnce();} break;} - if (dir > 0) if (!moveOnce()) break; + }); + }), + + removeLineClass: operation(null, function(handle, where, cls) { + return changeLine(this, handle, function(line) { + var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) return false; + else if (cls == null) line[prop] = null; + else { + var upd = cur.replace(new RegExp("^" + cls + "\\b\\s*|\\s*\\b" + cls + "\\b"), ""); + if (upd == cur) return false; + line[prop] = upd || null; } - } - return {line: line, ch: ch}; - } - function moveH(dir, unit) { - var pos = dir < 0 ? sel.from : sel.to; - if (shiftSelecting || posEq(sel.from, sel.to)) pos = findPosH(dir, unit); - setCursor(pos.line, pos.ch, true); - } - function deleteH(dir, unit) { - if (!posEq(sel.from, sel.to)) replaceRange("", sel.from, sel.to); - else if (dir < 0) replaceRange("", findPosH(dir, unit), sel.to); - else replaceRange("", sel.from, findPosH(dir, unit)); - userSelChange = true; - } - function moveV(dir, unit) { - var dist = 0, pos = localCoords(sel.inverted ? sel.from : sel.to, true); - if (goalColumn != null) pos.x = goalColumn; - if (unit == "page") { - var screen = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight); - var target = coordsChar(pos.x, pos.y + screen * dir); - } else if (unit == "line") { - var th = textHeight(); - var target = coordsChar(pos.x, pos.y + .5 * th + dir * th); - } - if (unit == "page") scrollbar.scrollTop += localCoords(target, true).y - pos.y; - setCursor(target.line, target.ch, true); - goalColumn = pos.x; - } - - function findWordAt(pos) { - var line = getLine(pos.line).text; - var start = pos.ch, end = pos.ch; - if (line) { - if (pos.after === false || end == line.length) --start; else ++end; - var startChar = line.charAt(start); - var check = isWordChar(startChar) ? isWordChar : - /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} : - function(ch) {return !/\s/.test(ch) && isWordChar(ch);}; - while (start > 0 && check(line.charAt(start - 1))) --start; - while (end < line.length && check(line.charAt(end))) ++end; - } - return {from: {line: pos.line, ch: start}, to: {line: pos.line, ch: end}}; - } - function selectLine(line) { - setSelectionUser({line: line, ch: 0}, clipPos({line: line + 1, ch: 0})); - } - 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); - } - - function indentLine(n, how) { - if (!how) how = "add"; - if (how == "smart") { - if (!mode.indent) how = "prev"; - else var state = getStateBefore(n); - } - - var line = getLine(n), curSpace = line.indentation(options.tabSize), - curSpaceString = line.text.match(/^\s*/)[0], indentation; - if (how == "smart") { - indentation = mode.indent(state, line.text.slice(curSpaceString.length), line.text); - if (indentation == Pass) how = "prev"; - } - if (how == "prev") { - if (n) indentation = getLine(n-1).indentation(options.tabSize); - else indentation = 0; - } - else if (how == "add") indentation = curSpace + options.indentUnit; - else if (how == "subtract") indentation = curSpace - options.indentUnit; - indentation = Math.max(0, indentation); - var diff = indentation - curSpace; - - var indentString = "", pos = 0; - if (options.indentWithTabs) - for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";} - if (pos < indentation) indentString += spaceStr(indentation - pos); - - if (indentString != curSpaceString) - replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length}); - line.stateAfter = null; - } - - function loadMode() { - mode = CodeMirror.getMode(options, options.mode); - doc.iter(0, doc.size, function(line) { line.stateAfter = null; }); - frontier = 0; - startWorker(100); - } - function gutterChanged() { - var visible = options.gutter || options.lineNumbers; - gutter.style.display = visible ? "" : "none"; - if (visible) gutterDirty = true; - else lineDiv.parentNode.style.marginLeft = 0; - } - function wrappingChanged(from, to) { - if (options.lineWrapping) { - wrapper.className += " CodeMirror-wrap"; - var perLine = scroller.clientWidth / charWidth() - 3; - doc.iter(0, doc.size, function(line) { - if (line.hidden) return; - var guess = Math.ceil(line.text.length / perLine) || 1; - if (guess != 1) updateLineHeight(line, guess); - }); - lineSpace.style.minWidth = widthForcer.style.left = ""; - } else { - wrapper.className = wrapper.className.replace(" CodeMirror-wrap", ""); - computeMaxLength(); - doc.iter(0, doc.size, function(line) { - if (line.height != 1 && !line.hidden) updateLineHeight(line, 1); - }); - } - changes.push({from: 0, to: doc.size}); - } - function themeChanged() { - scroller.className = scroller.className.replace(/\s*cm-s-\S+/g, "") + - options.theme.replace(/(^|\s)\s*/g, " cm-s-"); - } - function keyMapChanged() { - var style = keyMap[options.keyMap].style; - wrapper.className = wrapper.className.replace(/\s*cm-keymap-\S+/g, "") + - (style ? " cm-keymap-" + style : ""); - } - - function TextMarker(type, style) { this.lines = []; this.type = type; if (style) this.style = style; } - TextMarker.prototype.clear = operation(function() { - var min, max; - for (var i = 0; i < this.lines.length; ++i) { - var line = this.lines[i]; - var span = getMarkedSpanFor(line.markedSpans, this); - if (span.from != null) min = lineNo(line); - if (span.to != null) max = lineNo(line); - line.markedSpans = removeMarkedSpan(line.markedSpans, span); - } - if (min != null) changes.push({from: min, to: max + 1}); - this.lines.length = 0; - this.explicitlyCleared = true; - }); - TextMarker.prototype.find = function() { - var from, to; - for (var i = 0; i < this.lines.length; ++i) { - var line = this.lines[i]; - var span = getMarkedSpanFor(line.markedSpans, this); - if (span.from != null || span.to != null) { - var found = lineNo(line); - if (span.from != null) from = {line: found, ch: span.from}; - if (span.to != null) to = {line: found, ch: span.to}; - } - } - if (this.type == "bookmark") return from; - return from && {from: from, to: to}; - }; - - function markText(from, to, className, options) { - from = clipPos(from); to = clipPos(to); - var marker = new TextMarker("range", className); - if (options) for (var opt in options) if (options.hasOwnProperty(opt)) - marker[opt] = options[opt]; - var curLine = from.line; - doc.iter(curLine, to.line + 1, function(line) { - var span = {from: curLine == from.line ? from.ch : null, - to: curLine == to.line ? to.ch : null, - marker: marker}; - line.markedSpans = (line.markedSpans || []).concat([span]); - marker.lines.push(line); - ++curLine; + return true; }); - changes.push({from: from.line, to: to.line + 1}); - return marker; - } - - function setBookmark(pos) { - pos = clipPos(pos); - var marker = new TextMarker("bookmark"), line = getLine(pos.line); - history.addChange(pos.line, 1, [newHL(line.text, line.markedSpans)], true); - var span = {from: pos.ch, to: pos.ch, marker: marker}; - line.markedSpans = (line.markedSpans || []).concat([span]); - marker.lines.push(line); - return marker; - } - - function findMarksAt(pos) { - pos = clipPos(pos); - var markers = [], spans = getLine(pos.line).markedSpans; - if (spans) for (var i = 0; i < spans.length; ++i) { - var span = spans[i]; - if ((span.from == null || span.from <= pos.ch) && - (span.to == null || span.to >= pos.ch)) - markers.push(span.marker); - } - return markers; - } - - function addGutterMarker(line, text, className) { - if (typeof line == "number") line = getLine(clipLine(line)); - line.gutterMarker = {text: text, style: className}; - gutterDirty = true; - return line; - } - function removeGutterMarker(line) { - if (typeof line == "number") line = getLine(clipLine(line)); - line.gutterMarker = null; - gutterDirty = true; - } - - function changeLine(handle, op) { - var no = handle, line = handle; - if (typeof handle == "number") line = getLine(clipLine(handle)); - else no = lineNo(handle); - if (no == null) return null; - if (op(line, no)) changes.push({from: no, to: no + 1}); - else return null; - return line; - } - function setLineClass(handle, className, bgClassName) { - return changeLine(handle, function(line) { - if (line.className != className || line.bgClassName != bgClassName) { - line.className = className; - line.bgClassName = bgClassName; - return true; - } - }); - } - function setLineHidden(handle, hidden) { - return changeLine(handle, function(line, no) { - if (line.hidden != hidden) { - line.hidden = hidden; - if (!options.lineWrapping) { - if (hidden && line.text.length == maxLine.text.length) { - updateMaxLine = true; - } else if (!hidden && line.text.length > maxLine.text.length) { - maxLine = line; updateMaxLine = false; - } - } - updateLineHeight(line, hidden ? 0 : 1); - var fline = sel.from.line, tline = sel.to.line; - if (hidden && (fline == no || tline == no)) { - var from = fline == no ? skipHidden({line: fline, ch: 0}, fline, 0) : sel.from; - var to = tline == no ? skipHidden({line: tline, ch: 0}, tline, 0) : sel.to; - // Can't hide the last visible line, we'd have no place to put the cursor - if (!to) return; - setSelection(from, to); - } - return (gutterDirty = true); - } - }); - } - - function lineInfo(line) { + }), + + addLineWidget: operation(null, function(handle, node, options) { + return addLineWidget(this, handle, node, options); + }), + + removeLineWidget: function(widget) { widget.clear(); }, + + lineInfo: function(line) { if (typeof line == "number") { - if (!isLine(line)) return null; + if (!isLine(this.doc, line)) return null; var n = line; - line = getLine(line); + line = getLine(this.doc, line); if (!line) return null; } else { var n = lineNo(line); if (n == null) return null; } - var marker = line.gutterMarker; - return {line: n, handle: line, text: line.text, markerText: marker && marker.text, - markerClass: marker && marker.style, lineClass: line.className, bgClass: line.bgClassName}; - } - - function measureLine(line, ch) { - if (ch == 0) return {top: 0, left: 0}; - var pre = lineContent(line, ch); - removeChildrenAndAdd(measure, pre); - var anchor = pre.anchor; - var top = anchor.offsetTop, left = anchor.offsetLeft; - // Older IEs report zero offsets for spans directly after a wrap - if (ie && top == 0 && left == 0) { - var backup = elt("span", "x"); - anchor.parentNode.insertBefore(backup, anchor.nextSibling); - top = backup.offsetTop; - } - return {top: top, left: left}; - } - function localCoords(pos, inLineWrap) { - var x, lh = textHeight(), y = lh * (heightAtLine(doc, pos.line) - (inLineWrap ? displayOffset : 0)); - if (pos.ch == 0) x = 0; - else { - var sp = measureLine(getLine(pos.line), pos.ch); - x = sp.left; - if (options.lineWrapping) y += Math.max(0, sp.top); + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets}; + }, + + getViewport: function() { return {from: this.display.showingFrom, to: this.display.showingTo};}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + top = pos.top - node.offsetHeight; + else if (pos.bottom + node.offsetHeight <= vspace) + top = pos.bottom; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; } - return {x: x, y: y, yBot: y + lh}; - } - // Coords must be lineSpace-local - function coordsChar(x, y) { - var th = textHeight(), cw = charWidth(), heightPos = displayOffset + Math.floor(y / th); - if (heightPos < 0) return {line: 0, ch: 0}; - var lineNo = lineAtHeight(doc, heightPos); - if (lineNo >= doc.size) return {line: doc.size - 1, ch: getLine(doc.size - 1).text.length}; - var lineObj = getLine(lineNo), text = lineObj.text; - var tw = options.lineWrapping, innerOff = tw ? heightPos - heightAtLine(doc, lineNo) : 0; - if (x <= 0 && innerOff == 0) return {line: lineNo, ch: 0}; - var wrongLine = false; - function getX(len) { - var sp = measureLine(lineObj, len); - if (tw) { - var off = Math.round(sp.top / th); - wrongLine = off != innerOff; - return Math.max(0, sp.left + (off - innerOff) * scroller.clientWidth); - } - return sp.left; - } - var from = 0, fromX = 0, to = text.length, toX; - // Guess a suitable upper bound for our search. - var estimated = Math.min(to, Math.ceil((x + innerOff * scroller.clientWidth * .9) / cw)); - 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 {line: lineNo, ch: 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) { - var after = x - fromX < toX - x; - return {line: lineNo, ch: after ? from : to, after: after}; - } - var middle = Math.ceil((from + to) / 2), middleX = getX(middle); - if (middleX > x) {to = middle; toX = middleX; if (wrongLine) toX += 1000; } - else {from = middle; fromX = middleX;} + node.style.top = (top + paddingTop(display)) + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; + node.style.left = left + "px"; } - } - function pageCoords(pos) { - var local = localCoords(pos, true), off = eltOffset(lineSpace); - return {x: off.left + local.x, y: off.top + local.y, yBot: off.top + local.yBot}; - } - - var cachedHeight, cachedHeightFor, measurePre; - function textHeight() { - if (measurePre == null) { - measurePre = elt("pre"); - for (var i = 0; i < 49; ++i) { - measurePre.appendChild(document.createTextNode("x")); - measurePre.appendChild(elt("br")); - } - measurePre.appendChild(document.createTextNode("x")); + if (scroll) + scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + triggerOnKeyDown: operation(null, onKeyDown), + + execCommand: function(cmd) {return commands[cmd](this);}, + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) break; } - var offsetHeight = lineDiv.clientHeight; - if (offsetHeight == cachedHeightFor) return cachedHeight; - cachedHeightFor = offsetHeight; - removeChildrenAndAdd(measure, measurePre.cloneNode(true)); - cachedHeight = measure.firstChild.offsetHeight / 50 || 1; - removeChildren(measure); - return cachedHeight; - } - var cachedWidth, cachedWidthFor = 0; - function charWidth() { - if (scroller.clientWidth == cachedWidthFor) return cachedWidth; - cachedWidthFor = scroller.clientWidth; - var anchor = elt("span", "x"); - var pre = elt("pre", [anchor]); - removeChildrenAndAdd(measure, pre); - return (cachedWidth = anchor.offsetWidth || 10); - } - function paddingTop() {return lineSpace.offsetTop;} - function paddingLeft() {return lineSpace.offsetLeft;} - - function posFromMouse(e, liberal) { - 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); - return coordsChar(x - offL.left, y - offL.top); - } - var detectingSelectAll; - function onContextMenu(e) { - var pos = posFromMouse(e), scrollPos = scrollbar.scrollTop; - if (!pos || opera) return; // Opera is difficult. - if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to)) - operation(setCursor)(pos.line, pos.ch); - - var oldCSS = input.style.cssText; - 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);"; - focusInput(); - resetInput(true); - // Adds "Select all" to context menu in FF - if (posEq(sel.from, sel.to)) input.value = prevInput = " "; - - function rehide() { - inputDiv.style.position = "relative"; - input.style.cssText = oldCSS; - if (ie_lt9) scrollbar.scrollTop = scrollPos; - slowPoll(); - - // Try to detect the user choosing select-all - if (input.selectionStart != null) { - clearTimeout(detectingSelectAll); - var extval = input.value = " " + (posEq(sel.from, sel.to) ? "" : input.value), i = 0; - prevInput = " "; - input.selectionStart = 1; input.selectionEnd = extval.length; - detectingSelectAll = setTimeout(function poll(){ - if (prevInput == " " && input.selectionStart == 0) - operation(commands.selectAll)(instance); - else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500); - else resetInput(); - }, 200); - } - } - - if (gecko) { - e_stop(e); - var mouseup = connect(window, "mouseup", function() { - mouseup(); - setTimeout(rehide, 20); - }, true); - } else { - setTimeout(rehide, 50); + return cur; + }, + + moveH: operation(null, function(dir, unit) { + var sel = this.doc.sel, pos; + if (sel.shift || sel.extend || posEq(sel.from, sel.to)) + pos = findPosH(this.doc, sel.head, dir, unit, this.options.rtlMoveVisually); + else + pos = dir < 0 ? sel.from : sel.to; + extendSelection(this.doc, pos, pos, dir); + }), + + deleteH: operation(null, function(dir, unit) { + var sel = this.doc.sel; + if (!posEq(sel.from, sel.to)) replaceRange(this.doc, "", sel.from, sel.to, "+delete"); + else replaceRange(this.doc, "", sel.from, findPosH(this.doc, sel.head, dir, unit, false), "+delete"); + this.curOp.userSelChange = true; + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) x = coords.left; + else coords.left = x; + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) break; } - } - - // Cursor-blinking - function restartBlink() { - clearInterval(blinker); - var on = true; - cursor.style.visibility = ""; - blinker = setInterval(function() { - cursor.style.visibility = (on = !on) ? "" : "hidden"; - }, options.cursorBlinkRate); - } - - var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; - function matchBrackets(autoclear) { - var head = sel.inverted ? sel.from : sel.to, line = getLine(head.line), pos = head.ch - 1; - var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)]; - if (!match) return; - var ch = match.charAt(0), forward = match.charAt(1) == ">", d = forward ? 1 : -1, st = line.styles; - for (var off = pos + 1, i = 0, e = st.length; i < e; i+=2) - if ((off -= st[i].length) <= 0) {var style = st[i+1]; break;} - - var stack = [line.text.charAt(pos)], re = /[(){}[\]]/; - function scan(line, from, to) { - if (!line.text) return; - var st = line.styles, pos = forward ? 0 : line.text.length - 1, cur; - for (var i = forward ? 0 : st.length - 2, e = forward ? st.length : -2; i != e; i += 2*d) { - var text = st[i]; - if (st[i+1] != style) {pos += d * text.length; continue;} - for (var j = forward ? 0 : text.length - 1, te = forward ? text.length : -1; j != te; j += d, pos+=d) { - if (pos >= from && pos < to && re.test(cur = text.charAt(j))) { - var match = matching[cur]; - if (match.charAt(1) == ">" == forward) stack.push(cur); - else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false}; - else if (!stack.length) return {pos: pos, match: true}; - } - } - } - } - for (var i = head.line, e = forward ? Math.min(i + 100, doc.size) : Math.max(-1, i - 100); i != e; i+=d) { - var line = getLine(i), first = i == head.line; - var found = scan(line, first && forward ? pos + 1 : 0, first && !forward ? pos : line.text.length); - 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 - // find a line with a stateAfter, so that it can start with a - // valid state. If that fails, it returns the line with the - // smallest indentation, which tends to need the least context to - // parse correctly. - function findStartLine(n) { - var minindent, minline; - for (var search = n, lim = n - 40; search > lim; --search) { - if (search == 0) return 0; - var line = getLine(search-1); - if (line.stateAfter) return search; - var indented = line.indentation(options.tabSize); - if (minline == null || minindent > indented) { - minline = search - 1; - minindent = indented; - } + return cur; + }, + + moveV: operation(null, function(dir, unit) { + var sel = this.doc.sel; + var pos = cursorCoords(this, sel.head, "div"); + if (sel.goalColumn != null) pos.left = sel.goalColumn; + var target = findPosV(this, pos, dir, unit); + + if (unit == "page") addToScrollPos(this, 0, charCoords(this, target, "div").top - pos.top); + extendSelection(this.doc, target, target, dir); + sel.goalColumn = pos.left; + }), + + toggleOverwrite: function() { + if (this.state.overwrite = !this.state.overwrite) + this.display.cursor.className += " CodeMirror-overwrite"; + else + this.display.cursor.className = this.display.cursor.className.replace(" CodeMirror-overwrite", ""); + }, + hasFocus: function() { return this.state.focused; }, + + scrollTo: operation(null, function(x, y) { + updateScrollPos(this, x, y); + }), + getScrollInfo: function() { + var scroller = this.display.scroller, co = scrollerCutOff; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - co, width: scroller.scrollWidth - co, + clientHeight: scroller.clientHeight - co, clientWidth: scroller.clientWidth - co}; + }, + + scrollIntoView: operation(null, function(pos, margin) { + if (typeof pos == "number") pos = Pos(pos, 0); + if (!margin) margin = 0; + var coords = pos; + + if (!pos || pos.line != null) { + this.curOp.scrollToPos = pos ? clipPos(this.doc, pos) : this.doc.sel.head; + this.curOp.scrollToPosMargin = margin; + coords = cursorCoords(this, this.curOp.scrollToPos); } - return minline; - } - function getStateBefore(n) { - var pos = findStartLine(n), state = pos && getLine(pos-1).stateAfter; - if (!state) state = startState(mode); - else state = copyState(mode, state); - doc.iter(pos, n, function(line) { - line.process(mode, state, options.tabSize); - line.stateAfter = (pos == n - 1 || pos % 5 == 0) ? copyState(mode, state) : null; - }); - return state; - } - function highlightWorker() { - if (frontier >= showingTo) return; - var end = +new Date + options.workTime, state = copyState(mode, getStateBefore(frontier)); - var startFrontier = frontier; - doc.iter(frontier, showingTo, function(line) { - if (frontier >= showingFrom) { // Visible - line.highlight(mode, state, options.tabSize); - line.stateAfter = copyState(mode, state); - } else { - line.process(mode, state, options.tabSize); - line.stateAfter = frontier % 5 == 0 ? copyState(mode, state) : null; - } - ++frontier; - if (+new Date > end) { - startWorker(options.workDelay); - return true; - } - }); - if (showingTo > startFrontier && frontier >= showingFrom) - operation(function() {changes.push({from: startFrontier, to: frontier});})(); - } - function startWorker(time) { - if (frontier < showingTo) - highlight.set(time, highlightWorker); - } - - // Operations are used to wrap changes in such a way that each - // change won't have to update the cursor and display (which would - // be awkward, slow, and error-prone), but instead updates are - // batched and then all combined and executed at once. - function startOperation() { - updateInput = userSelChange = textChanged = null; - changes = []; selectionChanged = false; callbacks = []; - } - function endOperation() { - if (updateMaxLine) computeMaxLength(); - if (maxLineChanged && !options.lineWrapping) { - var cursorWidth = widthForcer.offsetWidth, left = measureLine(maxLine, maxLine.text.length).left; - if (!ie_lt8) { - widthForcer.style.left = left + "px"; - lineSpace.style.minWidth = (left + cursorWidth) + "px"; - } - maxLineChanged = false; + var sPos = calculateScrollPos(this, coords.left, coords.top - margin, coords.right, coords.bottom + margin); + updateScrollPos(this, sPos.scrollLeft, sPos.scrollTop); + }), + + setSize: function(width, height) { + function interpret(val) { + return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; } - var newScrollPos, updated; - if (selectionChanged) { - var coords = calculateCursorCoords(); - newScrollPos = calculateScrollPos(coords.x, coords.y, coords.x, coords.yBot); - } - if (changes.length || newScrollPos && newScrollPos.scrollTop != null) - updated = updateDisplay(changes, true, newScrollPos && newScrollPos.scrollTop); - if (!updated) { - if (selectionChanged) updateSelection(); - if (gutterDirty) updateGutter(); - } - if (newScrollPos) scrollCursorIntoView(); - if (selectionChanged) restartBlink(); - - if (focused && (updateInput === true || (updateInput !== false && selectionChanged))) - resetInput(userSelChange); - - if (selectionChanged && options.matchBrackets) - setTimeout(operation(function() { - if (bracketHighlighted) {bracketHighlighted(); bracketHighlighted = null;} - if (posEq(sel.from, sel.to)) matchBrackets(false); - }), 20); - var sc = selectionChanged, cbs = callbacks; // these can be reset by callbacks - if (textChanged && options.onChange && instance) - options.onChange(instance, textChanged); - if (sc && options.onCursorActivity) - options.onCursorActivity(instance); - for (var i = 0; i < cbs.length; ++i) cbs[i](instance); - if (updated && options.onUpdate) options.onUpdate(instance); - } - var nestedOperation = 0; - function operation(f) { - return function() { - if (!nestedOperation++) startOperation(); - try {var result = f.apply(this, arguments);} - finally {if (!--nestedOperation) endOperation();} - return result; - }; - } - - function compoundChange(f) { - history.startCompound(); - try { return f(); } finally { history.endCompound(); } - } - - for (var ext in extensions) - if (extensions.propertyIsEnumerable(ext) && - !instance.propertyIsEnumerable(ext)) - instance[ext] = extensions[ext]; - for (var i = 0; i < initHooks.length; ++i) initHooks[i](instance); - return instance; - } // (end of function CodeMirror) + if (width != null) this.display.wrapper.style.width = interpret(width); + if (height != null) this.display.wrapper.style.height = interpret(height); + this.refresh(); + }, + + on: function(type, f) {on(this, type, f);}, + off: function(type, f) {off(this, type, f);}, + + operation: function(f){return runInOp(this, f);}, + + refresh: operation(null, function() { + clearCaches(this); + updateScrollPos(this, this.doc.scrollLeft, this.doc.scrollTop); + regChange(this); + }), + + swapDoc: operation(null, function(doc) { + var old = this.doc; + old.cm = null; + attachDoc(this, doc); + clearCaches(this); + resetInput(this, true); + updateScrollPos(this, doc.scrollLeft, doc.scrollTop); + return old; + }), + + getInputField: function(){return this.display.input;}, + getWrapperElement: function(){return this.display.wrapper;}, + getScrollerElement: function(){return this.display.scroller;}, + getGutterElement: function(){return this.display.gutters;} + }; + + // OPTION DEFAULTS + + var optionHandlers = CodeMirror.optionHandlers = {}; // The default configuration options. - CodeMirror.defaults = { - value: "", - mode: null, - theme: "default", - indentUnit: 2, - indentWithTabs: false, - smartIndent: true, - tabSize: 4, - keyMap: "default", - extraKeys: null, - electricChars: true, - autoClearEmptyLines: false, - onKeyEvent: null, - onDragEvent: null, - lineWrapping: false, - lineNumbers: false, - gutter: false, - fixedGutter: false, - firstLineNumber: 1, - readOnly: false, - dragDrop: true, - onChange: null, - onCursorActivity: null, - onViewportChange: null, - onGutterClick: null, - onUpdate: null, - onFocus: null, onBlur: null, onScroll: null, - matchBrackets: false, - cursorBlinkRate: 530, - workTime: 100, - workDelay: 200, - pollInterval: 100, - undoDepth: 40, - tabindex: null, - autofocus: null, - lineNumberFormatter: function(integer) { return integer; } - }; - - var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); - var mac = ios || /Mac/.test(navigator.platform); - var win = /Win/.test(navigator.platform); + var defaults = CodeMirror.defaults = {}; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) optionHandlers[name] = + notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; + } + + var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function(cm, val) { + cm.setValue(val); + }, true); + option("mode", null, function(cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function(cm) { + loadMode(cm); + clearCaches(cm); + regChange(cm); + }, true); + option("electricChars", true); + option("rtlMoveVisually", !windows); + + option("theme", "default", function(cm) { + themeChanged(cm); + guttersChanged(cm); + }, true); + option("keyMap", "default", keyMapChanged); + option("extraKeys", null); + + option("onKeyEvent", null); + option("onDragEvent", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("fixedGutter", true, function(cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("lineNumbers", false, function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("firstLineNumber", 1, guttersChanged, true); + option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("readOnly", false, function(cm, val) { + if (val == "nocursor") {onBlur(cm); cm.display.input.blur();} + else if (!val) resetInput(cm, true); + }); + option("dragDrop", true); + + option("cursorBlinkRate", 530); + option("cursorHeight", 1); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true); + option("pollInterval", 100); + option("undoDepth", 40, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 500); + option("viewportMargin", 10, function(cm){cm.refresh();}, true); + option("maxHighlightLength", 10000, function(cm){loadMode(cm); cm.refresh();}, true); + option("moveInputWithCursor", true, function(cm, val) { + if (!val) cm.display.inputDiv.style.top = cm.display.inputDiv.style.left = 0; + }); + + option("tabindex", null, function(cm, val) { + cm.display.input.tabIndex = val || ""; + }); + option("autofocus", null); + + // MODE DEFINITION AND QUERYING // Known modes, by name and by MIME var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + CodeMirror.defineMode = function(name, mode) { if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; if (arguments.length > 2) { @@ -2042,9 +3157,11 @@ window.CodeMirror = (function() { } modes[name] = mode; }; + CodeMirror.defineMIME = function(mime, spec) { mimeModes[mime] = spec; }; + CodeMirror.resolveMode = function(spec) { if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) spec = mimeModes[spec]; @@ -2053,228 +3170,49 @@ window.CodeMirror = (function() { if (typeof spec == "string") return {name: spec}; else return spec || {name: "null"}; }; + CodeMirror.getMode = function(options, spec) { - var spec = CodeMirror.resolveMode(spec); + spec = CodeMirror.resolveMode(spec); var mfactory = modes[spec.name]; if (!mfactory) return CodeMirror.getMode(options, "text/plain"); var modeObj = mfactory(options, spec); if (modeExtensions.hasOwnProperty(spec.name)) { var exts = modeExtensions[spec.name]; - for (var prop in exts) if (exts.hasOwnProperty(prop)) modeObj[prop] = exts[prop]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) continue; + if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; + modeObj[prop] = exts[prop]; + } } modeObj.name = spec.name; return modeObj; }; - CodeMirror.listModes = function() { - var list = []; - for (var m in modes) - if (modes.propertyIsEnumerable(m)) list.push(m); - return list; - }; - CodeMirror.listMIMEs = function() { - var list = []; - for (var m in mimeModes) - if (mimeModes.propertyIsEnumerable(m)) list.push({mime: m, mode: mimeModes[m]}); - return list; - }; - - var extensions = CodeMirror.extensions = {}; - CodeMirror.defineExtension = function(name, func) { - extensions[name] = func; - }; - - var initHooks = []; - CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); var modeExtensions = CodeMirror.modeExtensions = {}; CodeMirror.extendMode = function(mode, properties) { var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); - for (var prop in properties) if (properties.hasOwnProperty(prop)) - exts[prop] = properties[prop]; + copyObj(properties, exts); }; - var commands = CodeMirror.commands = { - selectAll: function(cm) {cm.setSelection({line: 0, ch: 0}, {line: cm.lineCount() - 1});}, - killLine: function(cm) { - var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to); - if (!sel && cm.getLine(from.line).length == from.ch) cm.replaceRange("", from, {line: from.line + 1, ch: 0}); - else cm.replaceRange("", from, sel ? to : {line: from.line}); - }, - deleteLine: function(cm) {var l = cm.getCursor().line; cm.replaceRange("", {line: l, ch: 0}, {line: l});}, - undo: function(cm) {cm.undo();}, - redo: function(cm) {cm.redo();}, - goDocStart: function(cm) {cm.setCursor(0, 0, true);}, - goDocEnd: function(cm) {cm.setSelection({line: cm.lineCount() - 1}, null, true);}, - goLineStart: function(cm) {cm.setCursor(cm.getCursor().line, 0, true);}, - goLineStartSmart: function(cm) { - var cur = cm.getCursor(); - var text = cm.getLine(cur.line), firstNonWS = Math.max(0, text.search(/\S/)); - cm.setCursor(cur.line, cur.ch <= firstNonWS && cur.ch ? 0 : firstNonWS, true); - }, - goLineEnd: function(cm) {cm.setSelection({line: cm.getCursor().line}, null, true);}, - goLineUp: function(cm) {cm.moveV(-1, "line");}, - goLineDown: function(cm) {cm.moveV(1, "line");}, - goPageUp: function(cm) {cm.moveV(-1, "page");}, - goPageDown: function(cm) {cm.moveV(1, "page");}, - goCharLeft: function(cm) {cm.moveH(-1, "char");}, - goCharRight: function(cm) {cm.moveH(1, "char");}, - goColumnLeft: function(cm) {cm.moveH(-1, "column");}, - goColumnRight: function(cm) {cm.moveH(1, "column");}, - goWordLeft: function(cm) {cm.moveH(-1, "word");}, - goWordRight: function(cm) {cm.moveH(1, "word");}, - delCharLeft: function(cm) {cm.deleteH(-1, "char");}, - delCharRight: function(cm) {cm.deleteH(1, "char");}, - delWordLeft: function(cm) {cm.deleteH(-1, "word");}, - delWordRight: function(cm) {cm.deleteH(1, "word");}, - indentAuto: function(cm) {cm.indentSelection("smart");}, - indentMore: function(cm) {cm.indentSelection("add");}, - indentLess: function(cm) {cm.indentSelection("subtract");}, - insertTab: function(cm) {cm.replaceSelection("\t", "end");}, - defaultTab: function(cm) { - if (cm.somethingSelected()) cm.indentSelection("add"); - else cm.replaceSelection("\t", "end"); - }, - transposeChars: function(cm) { - var cur = cm.getCursor(), line = cm.getLine(cur.line); - if (cur.ch > 0 && cur.ch < line.length - 1) - cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1), - {line: cur.line, ch: cur.ch - 1}, {line: cur.line, ch: cur.ch + 1}); - }, - newlineAndIndent: function(cm) { - cm.replaceSelection("\n", "end"); - cm.indentLine(cm.getCursor().line); - }, - toggleOverwrite: function(cm) {cm.toggleOverwrite();} - }; - - var keyMap = CodeMirror.keyMap = {}; - keyMap.basic = { - "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", - "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", - "Delete": "delCharRight", "Backspace": "delCharLeft", "Tab": "defaultTab", "Shift-Tab": "indentAuto", - "Enter": "newlineAndIndent", "Insert": "toggleOverwrite" - }; - // Note that the save and find-related commands aren't defined by - // default. Unknown commands are simply ignored. - keyMap.pcDefault = { - "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", - "Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd", - "Ctrl-Left": "goWordLeft", "Ctrl-Right": "goWordRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", - "Ctrl-Backspace": "delWordLeft", "Ctrl-Delete": "delWordRight", "Ctrl-S": "save", "Ctrl-F": "find", - "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", - "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", - fallthrough: "basic" - }; - keyMap.macDefault = { - "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", - "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goWordLeft", - "Alt-Right": "goWordRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delWordLeft", - "Ctrl-Alt-Backspace": "delWordRight", "Alt-Delete": "delWordRight", "Cmd-S": "save", "Cmd-F": "find", - "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", - "Cmd-[": "indentLess", "Cmd-]": "indentMore", - fallthrough: ["basic", "emacsy"] - }; - keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; - keyMap.emacsy = { - "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", - "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", - "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharRight", "Ctrl-H": "delCharLeft", - "Alt-D": "delWordRight", "Alt-Backspace": "delWordLeft", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + // EXTENSIONS + + CodeMirror.defineExtension = function(name, func) { + CodeMirror.prototype[name] = func; }; - - function getKeyMap(val) { - if (typeof val == "string") return keyMap[val]; - else return val; - } - function lookupKey(name, extraMap, map, handle, stop) { - function lookup(map) { - map = getKeyMap(map); - var found = map[name]; - if (found === false) { - if (stop) stop(); - return true; - } - if (found != null && handle(found)) return true; - if (map.nofallthrough) { - if (stop) stop(); - return true; - } - var fallthrough = map.fallthrough; - if (fallthrough == null) return false; - if (Object.prototype.toString.call(fallthrough) != "[object Array]") - return lookup(fallthrough); - for (var i = 0, e = fallthrough.length; i < e; ++i) { - if (lookup(fallthrough[i])) return true; - } - return false; - } - if (extraMap && lookup(extraMap)) return true; - return lookup(map); - } - function isModifierKey(event) { - var name = keyNames[e_prop(event, "keyCode")]; - return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; - } - CodeMirror.isModifierKey = isModifierKey; - - CodeMirror.fromTextArea = function(textarea, options) { - if (!options) options = {}; - options.value = textarea.value; - if (!options.tabindex && textarea.tabindex) - options.tabindex = textarea.tabindex; - // Set autofocus to true if this textarea is focused, or if it has - // autofocus and no other element is focused. - if (options.autofocus == null) { - var hasFocus = document.body; - // doc.activeElement occasionally throws on IE - try { hasFocus = document.activeElement; } catch(e) {} - options.autofocus = hasFocus == textarea || - textarea.getAttribute("autofocus") != null && hasFocus == document.body; - } - - function save() {textarea.value = instance.getValue();} - if (textarea.form) { - // Deplorable hack to make the submit method do the right thing. - var rmSubmit = connect(textarea.form, "submit", save, true); - var realSubmit = textarea.form.submit; - textarea.form.submit = function wrappedSubmit() { - save(); - textarea.form.submit = realSubmit; - textarea.form.submit(); - textarea.form.submit = wrappedSubmit; - }; - } - - textarea.style.display = "none"; - var instance = CodeMirror(function(node) { - textarea.parentNode.insertBefore(node, textarea.nextSibling); - }, options); - instance.save = save; - instance.getTextArea = function() { return textarea; }; - instance.toTextArea = function() { - save(); - textarea.parentNode.removeChild(instance.getWrapperElement()); - textarea.style.display = ""; - if (textarea.form) { - rmSubmit(); - if (typeof textarea.form.submit == "function") - textarea.form.submit = realSubmit; - } - }; - return instance; + CodeMirror.defineDocExtension = function(name, func) { + Doc.prototype[name] = func; }; - - var gecko = /gecko\/\d{7}/i.test(navigator.userAgent); - var ie = /MSIE \d/.test(navigator.userAgent); - var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent); - var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent); - var quirksMode = ie && document.documentMode == 5; - var webkit = /WebKit\//.test(navigator.userAgent); - var chrome = /Chrome\//.test(navigator.userAgent); - var opera = /Opera\//.test(navigator.userAgent); - var safari = /Apple Computer/.test(navigator.vendor); - var khtml = /KHTML\//.test(navigator.userAgent); - var mac_geLion = /Mac OS X 10\D([7-9]|\d\d)\D/.test(navigator.userAgent); + CodeMirror.defineOption = option; + + var initHooks = []; + CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + + // MODE STATE HANDLING // Utility functions for working with state. Exported because modes // sometimes need to do this. @@ -2290,10 +3228,12 @@ window.CodeMirror = (function() { return nstate; } CodeMirror.copyState = copyState; + function startState(mode, a1, a2) { return mode.startState ? mode.startState(a1, a2) : true; } CodeMirror.startState = startState; + CodeMirror.innerMode = function(mode, state) { while (mode.innerMode) { var info = mode.innerMode(state); @@ -2303,12 +3243,243 @@ window.CodeMirror = (function() { return info || {mode: mode, state: state}; }; + // STANDARD COMMANDS + + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()));}, + killLine: function(cm) { + var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to); + if (!sel && cm.getLine(from.line).length == from.ch) + cm.replaceRange("", from, Pos(from.line + 1, 0), "+delete"); + else cm.replaceRange("", from, sel ? to : Pos(from.line), "+delete"); + }, + deleteLine: function(cm) { + var l = cm.getCursor().line; + cm.replaceRange("", Pos(l, 0), Pos(l), "+delete"); + }, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, + goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, + goLineStart: function(cm) { + cm.extendSelection(lineStart(cm, cm.getCursor().line)); + }, + goLineStartSmart: function(cm) { + var cur = cm.getCursor(), start = lineStart(cm, cur.line); + var line = cm.getLineHandle(start.line); + var order = getOrder(line); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = cur.line == start.line && cur.ch <= firstNonWS && cur.ch; + cm.extendSelection(Pos(start.line, inWS ? 0 : firstNonWS)); + } else cm.extendSelection(start); + }, + goLineEnd: function(cm) { + cm.extendSelection(lineEnd(cm, cm.getCursor().line)); + }, + goLineRight: function(cm) { + var top = cm.charCoords(cm.getCursor(), "div").top + 5; + cm.extendSelection(cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div")); + }, + goLineLeft: function(cm) { + var top = cm.charCoords(cm.getCursor(), "div").top + 5; + cm.extendSelection(cm.coordsChar({left: 0, top: top}, "div")); + }, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goGroupRight: function(cm) {cm.moveH(1, "group");}, + goGroupLeft: function(cm) {cm.moveH(-1, "group");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharBefore: function(cm) {cm.deleteH(-1, "char");}, + delCharAfter: function(cm) {cm.deleteH(1, "char");}, + delWordBefore: function(cm) {cm.deleteH(-1, "word");}, + delWordAfter: function(cm) {cm.deleteH(1, "word");}, + delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, + delGroupAfter: function(cm) {cm.deleteH(1, "group");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t", "end", "+input");}, + defaultTab: function(cm) { + if (cm.somethingSelected()) cm.indentSelection("add"); + else cm.replaceSelection("\t", "end", "+input"); + }, + transposeChars: function(cm) { + var cur = cm.getCursor(), line = cm.getLine(cur.line); + if (cur.ch > 0 && cur.ch < line.length - 1) + cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1), + Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1)); + }, + newlineAndIndent: function(cm) { + operation(cm, function() { + cm.replaceSelection("\n", "end", "+input"); + cm.indentLine(cm.getCursor().line, null, true); + })(); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + // STANDARD KEYMAPS + + var keyMap = CodeMirror.keyMap = {}; + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite" + }; + // Note that the save and find-related commands aren't defined by + // default. Unknown commands are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + fallthrough: "basic" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + + // KEYMAP DISPATCH + + function getKeyMap(val) { + if (typeof val == "string") return keyMap[val]; + else return val; + } + + function lookupKey(name, maps, handle) { + function lookup(map) { + map = getKeyMap(map); + var found = map[name]; + if (found === false) return "stop"; + if (found != null && handle(found)) return true; + if (map.nofallthrough) return "stop"; + + var fallthrough = map.fallthrough; + if (fallthrough == null) return false; + if (Object.prototype.toString.call(fallthrough) != "[object Array]") + return lookup(fallthrough); + for (var i = 0, e = fallthrough.length; i < e; ++i) { + var done = lookup(fallthrough[i]); + if (done) return done; + } + return false; + } + + for (var i = 0; i < maps.length; ++i) { + var done = lookup(maps[i]); + if (done) return done; + } + } + function isModifierKey(event) { + var name = keyNames[event.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + } + function keyName(event, noShift) { + if (opera && event.keyCode == 34 && event["char"]) return false; + var name = keyNames[event.keyCode]; + if (name == null || event.altGraphKey) return false; + if (event.altKey) name = "Alt-" + name; + if (flipCtrlCmd ? event.metaKey : event.ctrlKey) name = "Ctrl-" + name; + if (flipCtrlCmd ? event.ctrlKey : event.metaKey) name = "Cmd-" + name; + if (!noShift && event.shiftKey) name = "Shift-" + name; + return name; + } + CodeMirror.lookupKey = lookupKey; + CodeMirror.isModifierKey = isModifierKey; + CodeMirror.keyName = keyName; + + // FROMTEXTAREA + + CodeMirror.fromTextArea = function(textarea, options) { + if (!options) options = {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabindex) + options.tabindex = textarea.tabindex; + if (!options.placeholder && textarea.placeholder) + options.placeholder = textarea.placeholder; + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = document.body; + // doc.activeElement occasionally throws on IE + try { hasFocus = document.activeElement; } catch(e) {} + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form, realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function() { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + textarea.style.display = "none"; + var cm = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + cm.save = save; + cm.getTextArea = function() { return textarea; }; + cm.toTextArea = function() { + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + return cm; + }; + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + // The character stream used by a mode's parser. function StringStream(string, tabSize) { this.pos = this.start = 0; this.string = string; this.tabSize = tabSize || 8; - } + this.lastColumnPos = this.lastColumnValue = 0; + } + StringStream.prototype = { eol: function() {return this.pos >= this.string.length;}, sol: function() {return this.pos == 0;}, @@ -2339,12 +3510,19 @@ window.CodeMirror = (function() { if (found > -1) {this.pos = found; return true;} }, backUp: function(n) {this.pos -= n;}, - column: function() {return countColumn(this.string, this.start, this.tabSize);}, + column: function() { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue; + }, indentation: function() {return countColumn(this.string, null, this.tabSize);}, match: function(pattern, consume, caseInsensitive) { if (typeof pattern == "string") { var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; - if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) { + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { if (consume !== false) this.pos += pattern.length; return true; } @@ -2359,9 +3537,195 @@ window.CodeMirror = (function() { }; CodeMirror.StringStream = StringStream; - function MarkedSpan(from, to, marker) { - this.from = from; this.to = to; this.marker = marker; - } + // TEXTMARKERS + + function TextMarker(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + } + CodeMirror.TextMarker = TextMarker; + + TextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) startOperation(cm); + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.to != null) max = lineNo(line); + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from != null) + min = lineNo(line); + else if (this.collapsed && !lineIsHidden(this.doc, line) && cm) + updateLineHeight(line, textHeight(cm.display)); + } + if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { + var visual = visualLine(cm.doc, this.lines[i]), len = lineLength(cm.doc, visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } + + if (min != null && cm) regChange(cm, min, max + 1); + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.collapsed && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) reCheckSelection(cm); + } + if (withOp) endOperation(cm); + signalLater(this, "clear"); + }; + + TextMarker.prototype.find = function() { + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null || span.to != null) { + var found = lineNo(line); + if (span.from != null) from = Pos(found, span.from); + if (span.to != null) to = Pos(found, span.to); + } + } + if (this.type == "bookmark") return from; + return from && {from: from, to: to}; + }; + + TextMarker.prototype.getOptions = function(copyWidget) { + var repl = this.replacedWith; + return {className: this.className, + inclusiveLeft: this.inclusiveLeft, inclusiveRight: this.inclusiveRight, + atomic: this.atomic, + collapsed: this.collapsed, + replacedWith: copyWidget ? repl && repl.cloneNode(true) : repl, + readOnly: this.readOnly, + startStyle: this.startStyle, endStyle: this.endStyle}; + }; + + TextMarker.prototype.attachLine = function(line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); + } + this.lines.push(line); + }; + TextMarker.prototype.detachLine = function(line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + + function markText(doc, from, to, options, type) { + if (options && options.shared) return markTextShared(doc, from, to, options, type); + if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); + + var marker = new TextMarker(doc, type); + if (type == "range" && !posLess(from, to)) return marker; + if (options) copyObj(options, marker); + if (marker.replacedWith) { + marker.collapsed = true; + marker.replacedWith = elt("span", [marker.replacedWith], "CodeMirror-widget"); + } + if (marker.collapsed) sawCollapsedSpans = true; + + if (marker.addToHistory) + addToHistory(doc, {from: from, to: to, origin: "markText"}, + {head: doc.sel.head, anchor: doc.sel.anchor}, NaN); + + var curLine = from.line, size = 0, collapsedAtStart, collapsedAtEnd, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function(line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(doc, line) == cm.display.maxLine) + updateMaxLine = true; + var span = {from: null, to: null, marker: marker}; + size += line.text.length; + if (curLine == from.line) {span.from = from.ch; size -= from.ch;} + if (curLine == to.line) {span.to = to.ch; size -= line.text.length - to.ch;} + if (marker.collapsed) { + if (curLine == to.line) collapsedAtEnd = collapsedSpanAt(line, to.ch); + if (curLine == from.line) collapsedAtStart = collapsedSpanAt(line, from.ch); + else updateLineHeight(line, 0); + } + addMarkedSpan(line, span); + ++curLine; + }); + if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { + if (lineIsHidden(doc, line)) updateLineHeight(line, 0); + }); + + if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); + + if (marker.readOnly) { + sawReadOnlySpans = true; + if (doc.history.done.length || doc.history.undone.length) + doc.clearHistory(); + } + if (marker.collapsed) { + if (collapsedAtStart != collapsedAtEnd) + throw new Error("Inserting collapsed marker overlapping an existing one"); + marker.size = size; + marker.atomic = true; + } + if (cm) { + if (updateMaxLine) cm.curOp.updateMaxLine = true; + if (marker.className || marker.startStyle || marker.endStyle || marker.collapsed) + regChange(cm, from.line, to.line + 1); + if (marker.atomic) reCheckSelection(cm); + } + return marker; + } + + // SHARED TEXTMARKERS + + function SharedTextMarker(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0, me = this; i < markers.length; ++i) { + markers[i].parent = this; + on(markers[i], "clear", function(){me.clear();}); + } + } + CodeMirror.SharedTextMarker = SharedTextMarker; + + SharedTextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + this.markers[i].clear(); + signalLater(this, "clear"); + }; + SharedTextMarker.prototype.find = function() { + return this.primary.find(); + }; + SharedTextMarker.prototype.getOptions = function(copyWidget) { + var inner = this.primary.getOptions(copyWidget); + inner.shared = true; + return inner; + }; + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.replacedWith; + linkedDocs(doc, function(doc) { + if (widget) options.replacedWith = widget.cloneNode(true); + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + if (doc.linked[i].isParent) return; + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary); + } + + // TEXTMARKER SPANS function getMarkedSpanFor(spans, marker) { if (spans) for (var i = 0; i < spans.length; ++i) { @@ -2369,19 +3733,21 @@ window.CodeMirror = (function() { if (span.marker == marker) return span; } } - function removeMarkedSpan(spans, span) { - var r; - for (var i = 0; i < spans.length; ++i) + for (var r, i = 0; i < spans.length; ++i) if (spans[i] != span) (r || (r = [])).push(spans[i]); return r; } - - function markedSpansBefore(old, startCh, endCh) { + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + function markedSpansBefore(old, startCh, isInsert) { if (old) for (var i = 0, nw; i < old.length; ++i) { var span = old[i], marker = span.marker; var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); - if (startsBefore || marker.type == "bookmark" && span.from == startCh && span.from != endCh) { + if (startsBefore || marker.type == "bookmark" && span.from == startCh && (!isInsert || !span.marker.insertLeft)) { var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); (nw || (nw = [])).push({from: span.from, to: endsAfter ? null : span.to, @@ -2391,11 +3757,11 @@ window.CodeMirror = (function() { return nw; } - function markedSpansAfter(old, endCh) { + function markedSpansAfter(old, endCh, isInsert) { if (old) for (var i = 0, nw; i < old.length; ++i) { var span = old[i], marker = span.marker; var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); - if (endsAfter || marker.type == "bookmark" && span.from == endCh) { + if (endsAfter || marker.type == "bookmark" && span.from == endCh && (!isInsert || span.marker.insertLeft)) { var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); (nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh, to: span.to == null ? null : span.to - endCh, @@ -2405,14 +3771,18 @@ window.CodeMirror = (function() { return nw; } - function updateMarkedSpans(oldFirst, oldLast, startCh, endCh, newText) { - if (!oldFirst && !oldLast) return newText; + function stretchSpansOverChange(doc, change) { + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) return null; + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = posEq(change.from, change.to); // Get the spans that 'stick out' on both sides - var first = markedSpansBefore(oldFirst, startCh); - var last = markedSpansAfter(oldLast, endCh); + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); // Next, merge those two ends - var sameLine = newText.length == 1, offset = lst(newText).length + (sameLine ? startCh : 0); + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); if (first) { // Fix up .to properties of first for (var i = 0; i < first.length; ++i) { @@ -2442,261 +3812,560 @@ window.CodeMirror = (function() { } } - var newMarkers = [newHL(newText[0], first)]; + var newMarkers = [first]; if (!sameLine) { // Fill gap with whole-line-spans - var gap = newText.length - 2, gapMarkers; + var gap = change.text.length - 2, gapMarkers; if (gap > 0 && first) for (var i = 0; i < first.length; ++i) if (first[i].to == null) (gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker}); for (var i = 0; i < gap; ++i) - newMarkers.push(newHL(newText[i+1], gapMarkers)); - newMarkers.push(newHL(lst(newText), last)); + newMarkers.push(gapMarkers); + newMarkers.push(last); } return newMarkers; } - // hl stands for history-line, a data structure that can be either a - // string (line without markers) or a {text, markedSpans} object. - function hlText(val) { return typeof val == "string" ? val : val.text; } - function hlSpans(val) { - if (typeof val == "string") return null; - var spans = val.markedSpans, out = null; - for (var i = 0; i < spans.length; ++i) { - if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } - else if (out) out.push(spans[i]); - } - return !out ? spans : out.length ? out : null; - } - function newHL(text, spans) { return spans ? {text: text, markedSpans: spans} : text; } + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) return stretched; + if (!stretched) return old; + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + if (oldCur[k].marker == span.marker) continue spans; + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old; + } + + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function(line) { + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + (markers || (markers = [])).push(mark); + } + }); + if (!markers) return null; + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (posLess(p.to, m.from) || posLess(m.to, p.from)) continue; + var newParts = [j, 1]; + if (posLess(p.from, m.from) || !mk.inclusiveLeft && posEq(p.from, m.from)) + newParts.push({from: p.from, to: m.from}); + if (posLess(m.to, p.to) || !mk.inclusiveRight && posEq(p.to, m.to)) + newParts.push({from: m.to, to: p.to}); + parts.splice.apply(parts, newParts); + j += newParts.length - 1; + } + } + return parts; + } + + function collapsedSpanAt(line, ch) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if ((sp.from == null || sp.from < ch) && + (sp.to == null || sp.to > ch) && + (!found || found.width < sp.marker.width)) + found = sp.marker; + } + return found; + } + function collapsedSpanAtStart(line) { return collapsedSpanAt(line, -1); } + function collapsedSpanAtEnd(line) { return collapsedSpanAt(line, line.text.length + 1); } + + function visualLine(doc, line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + line = getLine(doc, merged.find().from.line); + return line; + } + + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if (sp.from == null) return true; + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + return true; + } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find().to, endLine = getLine(doc, end.line); + return lineIsHiddenInner(doc, endLine, getMarkedSpanFor(endLine.markedSpans, span.marker)); + } + if (span.marker.inclusiveRight && span.to == line.text.length) + return true; + for (var sp, i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && sp.from == span.to && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) return true; + } + } function detachMarkedSpans(line) { var spans = line.markedSpans; if (!spans) return; - for (var i = 0; i < spans.length; ++i) { - var lines = spans[i].marker.lines; - var ix = indexOf(lines, line); - lines.splice(ix, 1); - } + for (var i = 0; i < spans.length; ++i) + spans[i].marker.detachLine(line); line.markedSpans = null; } function attachMarkedSpans(line, spans) { if (!spans) return; for (var i = 0; i < spans.length; ++i) - var marker = spans[i].marker.lines.push(line); + spans[i].marker.attachLine(line); line.markedSpans = spans; } - // When measuring the position of the end of a line, different - // browsers require different approaches. If an empty span is added, - // many browsers report bogus offsets. Of those, some (Webkit, - // recent IE) will accept a space without moving the whole span to - // the next line when wrapping it, others work with a zero-width - // space. - var eolSpanContent = " "; - if (gecko || (ie && !ie_lt8)) eolSpanContent = "\u200b"; - else if (opera) eolSpanContent = ""; + // LINE WIDGETS + + var LineWidget = CodeMirror.LineWidget = function(cm, node, options) { + for (var opt in options) if (options.hasOwnProperty(opt)) + this[opt] = options[opt]; + this.cm = cm; + this.node = node; + }; + function widgetOperation(f) { + return function() { + var withOp = !this.cm.curOp; + if (withOp) startOperation(this.cm); + try {var result = f.apply(this, arguments);} + finally {if (withOp) endOperation(this.cm);} + return result; + }; + } + LineWidget.prototype.clear = widgetOperation(function() { + var ws = this.line.widgets, no = lineNo(this.line); + if (no == null || !ws) return; + for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); + if (!ws.length) this.line.widgets = null; + updateLineHeight(this.line, Math.max(0, this.line.height - widgetHeight(this))); + regChange(this.cm, no, no + 1); + }); + LineWidget.prototype.changed = widgetOperation(function() { + var oldH = this.height; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) return; + updateLineHeight(this.line, this.line.height + diff); + var no = lineNo(this.line); + regChange(this.cm, no, no + 1); + }); + + function widgetHeight(widget) { + if (widget.height != null) return widget.height; + if (!widget.node.parentNode || widget.node.parentNode.nodeType != 1) + removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, "position: relative")); + return widget.height = widget.node.offsetHeight; + } + + function addLineWidget(cm, handle, node, options) { + var widget = new LineWidget(cm, node, options); + if (widget.noHScroll) cm.display.alignWidgets = true; + changeLine(cm, handle, function(line) { + (line.widgets || (line.widgets = [])).push(widget); + widget.line = line; + if (!lineIsHidden(cm.doc, line) || widget.showIfHidden) { + var aboveVisible = heightAtLine(cm, line) < cm.display.scroller.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) addToScrollPos(cm, 0, widget.height); + } + return true; + }); + return widget; + } + + // LINE DATA STRUCTURE // Line objects. These hold state related to a line, including // highlighting info (the styles array). - function Line(text, markedSpans) { - this.text = text; - this.height = 1; - attachMarkedSpans(this, markedSpans); - } - Line.prototype = { - update: function(text, markedSpans) { - this.text = text; - this.stateAfter = this.styles = null; - detachMarkedSpans(this); - attachMarkedSpans(this, markedSpans); - }, - // Run the given mode's parser over a line, update the styles - // array, which contains alternating fragments of text and CSS - // classes. - highlight: function(mode, state, tabSize) { - var stream = new StringStream(this.text, tabSize), st = this.styles || (this.styles = []); - var pos = st.length = 0; - if (this.text == "" && mode.blankLine) mode.blankLine(state); - while (!stream.eol()) { - var style = mode.token(stream, state), substr = stream.current(); - stream.start = stream.pos; - if (pos && st[pos-1] == style) { - st[pos-2] += substr; - } else if (substr) { - st[pos++] = substr; st[pos++] = style; - } - // Give up when line is ridiculously long - if (stream.pos > 5000) { - st[pos++] = this.text.slice(stream.pos); st[pos++] = null; - break; - } - } - }, - process: function(mode, state, tabSize) { - var stream = new StringStream(this.text, tabSize); - if (this.text == "" && mode.blankLine) mode.blankLine(state); - while (!stream.eol() && stream.pos <= 5000) { - mode.token(stream, state); - stream.start = stream.pos; + function makeLine(text, markedSpans, estimateHeight) { + var line = {text: text}; + attachMarkedSpans(line, markedSpans); + line.height = estimateHeight ? estimateHeight(line) : 1; + return line; + } + + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + if (line.order != null) line.order = null; + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) updateLineHeight(line, estHeight); + } + + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + // Run the given mode's parser over a line, update the styles + // array, which contains alternating fragments of text and CSS + // classes. + function runMode(cm, text, mode, state, f) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; + var curText = "", curStyle = null; + var stream = new StringStream(text, cm.options.tabSize), style; + if (text == "" && mode.blankLine) mode.blankLine(state); + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + // Webkit seems to refuse to render text nodes longer than 57444 characters + stream.pos = Math.min(text.length, stream.start + 50000); + style = null; + } else { + style = mode.token(stream, state); } - }, - // Fetch the parser token for a given character. Useful for hacks - // that want to inspect the mode state (say, for completion). - getTokenAt: function(mode, state, tabSize, ch) { - var txt = this.text, stream = new StringStream(txt, tabSize); - while (stream.pos < ch && !stream.eol()) { - stream.start = stream.pos; - var style = mode.token(stream, state); - } - return {start: stream.start, - end: stream.pos, - string: stream.current(), - className: style || null, - state: state}; - }, - indentation: function(tabSize) {return countColumn(this.text, null, tabSize);}, - // Produces an HTML fragment for the line, taking selection, - // marking, and highlighting into account. - getContent: function(tabSize, wrapAt, compensateForWrapping) { - var first = true, col = 0, specials = /[\t\u0000-\u0019\u200b\u2028\u2029\uFEFF]/g; - var pre = elt("pre"); - function span_(html, text, style) { - if (!text) return; - // Work around a bug where, in some compat modes, IE ignores leading spaces - if (first && ie && text.charAt(0) == " ") text = "\u00a0" + text.slice(1); - first = false; - if (!specials.test(text)) { - col += text.length; - var content = document.createTextNode(text); + var substr = stream.current(); + stream.start = stream.pos; + if (!flattenSpans || curStyle != style) { + if (curText) f(curText, curStyle); + curText = substr; curStyle = style; + } else curText = curText + substr; + } + if (curText) f(curText, curStyle); + } + + function highlightLine(cm, line, state) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen]; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, state, function(txt, style) {st.push(txt, style);}); + + // Run overlays, adjust style array. + for (var o = 0; o < cm.state.overlays.length; ++o) { + var overlay = cm.state.overlays[o], i = 1; + runMode(cm, line.text, overlay.mode, true, function(txt, style) { + var start = i, len = txt.length; + // Ensure there's a token end at the current position, and that i points at it + while (len) { + var cur = st[i], len_ = cur.length; + if (len_ <= len) { + len -= len_; + } else { + st.splice(i, 1, cur.slice(0, len), st[i+1], cur.slice(len)); + len = 0; + } + i += 2; + } + if (!style) return; + if (overlay.opaque) { + st.splice(start, i - start, txt, style); + i = start + 2; } else { - var content = document.createDocumentFragment(), pos = 0; - while (true) { - specials.lastIndex = pos; - var m = specials.exec(text); - var skipped = m ? m.index - pos : text.length - pos; - if (skipped) { - content.appendChild(document.createTextNode(text.slice(pos, pos + skipped))); - col += skipped; - } - if (!m) break; - pos += skipped + 1; - if (m[0] == "\t") { - var tabWidth = tabSize - col % tabSize; - content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); - col += tabWidth; - } else { - var token = elt("span", "\u2022", "cm-invalidchar"); - token.title = "\\u" + m[0].charCodeAt(0).toString(16); - content.appendChild(token); - col += 1; - } + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = cur ? cur + " " + style : style; } } - if (style) html.appendChild(elt("span", [content], style)); - else html.appendChild(content); - } - var span = span_; - if (wrapAt != null) { - var outPos = 0, anchor = pre.anchor = elt("span"); - span = function(html, text, style) { - var l = text.length; - if (wrapAt >= outPos && wrapAt < outPos + l) { - var cut = wrapAt - outPos; - if (cut) { - span_(html, text.slice(0, cut), style); - // See comment at the definition of spanAffectsWrapping - if (compensateForWrapping) { - var view = text.slice(cut - 1, cut + 1); - if (spanAffectsWrapping.test(view)) html.appendChild(elt("wbr")); - else if (!ie_lt8 && /\w\w/.test(view)) html.appendChild(document.createTextNode("\u200d")); - } - } - html.appendChild(anchor); - span_(anchor, opera ? text.slice(cut, cut + 1) : text.slice(cut), style); - if (opera) span_(html, text.slice(cut + 1), style); - wrapAt--; - outPos += l; - } else { - outPos += l; - span_(html, text, style); - if (outPos == wrapAt && outPos == len) { - setTextContent(anchor, eolSpanContent); - html.appendChild(anchor); - } - // Stop outputting HTML when gone sufficiently far beyond measure - else if (outPos > wrapAt + 10 && /\s/.test(text)) span = function(){}; - } - }; - } - - var st = this.styles, allText = this.text, marked = this.markedSpans; - var len = allText.length; - function styleToClass(style) { - if (!style) return null; - return "cm-" + style.replace(/ +/g, " cm-"); + }); + } + + return st; + } + + function getLineStyles(cm, line) { + if (!line.styles || line.styles[0] != cm.state.modeGen) + line.styles = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); + return line.styles; + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. + function processLine(cm, line, state) { + var mode = cm.doc.mode; + var stream = new StringStream(line.text, cm.options.tabSize); + if (line.text == "" && mode.blankLine) mode.blankLine(state); + while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { + mode.token(stream, state); + stream.start = stream.pos; + } + } + + var styleToClassCache = {}; + function styleToClass(style) { + if (!style) return null; + return styleToClassCache[style] || + (styleToClassCache[style] = "cm-" + style.replace(/ +/g, " cm-")); + } + + function lineContent(cm, realLine, measure) { + var merged, line = realLine, lineBefore, sawBefore, simple = true; + while (merged = collapsedSpanAtStart(line)) { + simple = false; + line = getLine(cm.doc, merged.find().from.line); + if (!lineBefore) lineBefore = line; + } + + var builder = {pre: elt("pre"), col: 0, pos: 0, display: !measure, + measure: null, addedOne: false, cm: cm}; + if (line.textClass) builder.pre.className = line.textClass; + + do { + builder.measure = line == realLine && measure; + builder.pos = 0; + builder.addToken = builder.measure ? buildTokenMeasure : buildToken; + if ((ie || webkit) && cm.getOption("lineWrapping")) + builder.addToken = buildTokenSplitSpaces(builder.addToken); + if (measure && sawBefore && line != realLine && !builder.addedOne) { + measure[0] = builder.pre.appendChild(zeroWidthElement(cm.display.measure)); + builder.addedOne = true; } - if (!allText && wrapAt == null) { - span(pre, " "); - } else if (!marked || !marked.length) { - for (var i = 0, ch = 0; ch < len; i+=2) { - var str = st[i], style = st[i+1], l = str.length; - if (ch + l > len) str = str.slice(0, len - ch); - ch += l; - span(pre, str, styleToClass(style)); + var next = insertLineContent(line, builder, getLineStyles(cm, line)); + sawBefore = line == lineBefore; + if (next) { + line = getLine(cm.doc, next.to.line); + simple = false; + } + } while (next); + + if (measure && !builder.addedOne) + measure[0] = builder.pre.appendChild(simple ? elt("span", "\u00a0") : zeroWidthElement(cm.display.measure)); + if (!builder.pre.firstChild && !lineIsHidden(cm.doc, realLine)) + builder.pre.appendChild(document.createTextNode("\u00a0")); + + var order; + // Work around problem with the reported dimensions of single-char + // direction spans on IE (issue #1129). See also the comment in + // cursorCoords. + if (measure && ie && (order = getOrder(line))) { + var l = order.length - 1; + if (order[l].from == order[l].to) --l; + var last = order[l], prev = order[l - 1]; + if (last.from + 1 == last.to && prev && last.level < prev.level) { + var span = measure[builder.pos - 1]; + if (span) span.parentNode.insertBefore(span.measureRight = zeroWidthElement(cm.display.measure), + span.nextSibling); + } + } + + signal(cm, "renderLine", cm, realLine, builder.pre); + return builder.pre; + } + + var tokenSpecialChars = /[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\uFEFF]/g; + function buildToken(builder, text, style, startStyle, endStyle) { + if (!text) return; + if (!tokenSpecialChars.test(text)) { + builder.col += text.length; + var content = document.createTextNode(text); + } else { + var content = document.createDocumentFragment(), pos = 0; + while (true) { + tokenSpecialChars.lastIndex = pos; + var m = tokenSpecialChars.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + content.appendChild(document.createTextNode(text.slice(pos, pos + skipped))); + builder.col += skipped; } - } else { - marked.sort(function(a, b) { return a.from - b.from; }); - var pos = 0, i = 0, text = "", style, sg = 0; - var nextChange = marked[0].from || 0, marks = [], markpos = 0; - var advanceMarks = function() { - var m; - while (markpos < marked.length && - ((m = marked[markpos]).from == pos || m.from == null)) { - if (m.marker.type == "range") marks.push(m); - ++markpos; - } - nextChange = markpos < marked.length ? marked[markpos].from : Infinity; - for (var i = 0; i < marks.length; ++i) { - var to = marks[i].to; - if (to == null) to = Infinity; - if (to == pos) marks.splice(i--, 1); - else nextChange = Math.min(to, nextChange); - } - }; - var m = 0; - while (pos < len) { - if (nextChange == pos) advanceMarks(); - var upto = Math.min(len, nextChange); - while (true) { - if (text) { - var end = pos + text.length; - var appliedStyle = style; - for (var j = 0; j < marks.length; ++j) { - var mark = marks[j]; - appliedStyle = (appliedStyle ? appliedStyle + " " : "") + mark.marker.style; - if (mark.marker.endStyle && mark.to === Math.min(end, upto)) appliedStyle += " " + mark.marker.endStyle; - if (mark.marker.startStyle && mark.from === pos) appliedStyle += " " + mark.marker.startStyle; - } - span(pre, end > upto ? text.slice(0, upto - pos) : text, appliedStyle); - if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} - pos = end; - } - text = st[i++]; style = styleToClass(st[i++]); - } + if (!m) break; + pos += skipped + 1; + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + builder.col += tabWidth; + } else { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + m[0].charCodeAt(0).toString(16); + content.appendChild(token); + builder.col += 1; } } - return pre; - }, - cleanUp: function() { - this.parent = null; - detachMarkedSpans(this); - } - }; - - // Data structure that holds the sequence of lines. + } + if (style || startStyle || endStyle || builder.measure) { + var fullStyle = style || ""; + if (startStyle) fullStyle += startStyle; + if (endStyle) fullStyle += endStyle; + return builder.pre.appendChild(elt("span", [content], fullStyle)); + } + builder.pre.appendChild(content); + } + + function buildTokenMeasure(builder, text, style, startStyle, endStyle) { + var wrapping = builder.cm.options.lineWrapping; + for (var i = 0; i < text.length; ++i) { + var ch = text.charAt(i), start = i == 0; + if (ch >= "\ud800" && ch < "\udbff" && i < text.length - 1) { + ch = text.slice(i, i + 2); + ++i; + } else if (i && wrapping && + spanAffectsWrapping.test(text.slice(i - 1, i + 1))) { + builder.pre.appendChild(elt("wbr")); + } + var span = builder.measure[builder.pos] = + buildToken(builder, ch, style, + start && startStyle, i == text.length - 1 && endStyle); + // In IE single-space nodes wrap differently than spaces + // embedded in larger text nodes, except when set to + // white-space: normal (issue #1268). + if (ie && wrapping && ch == " " && i && !/\s/.test(text.charAt(i - 1)) && + i < text.length - 1 && !/\s/.test(text.charAt(i + 1))) + span.style.whiteSpace = "normal"; + builder.pos += ch.length; + } + if (text.length) builder.addedOne = true; + } + + function buildTokenSplitSpaces(inner) { + function split(old) { + var out = " "; + for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; + out += " "; + return out; + } + return function(builder, text, style, startStyle, endStyle) { + return inner(builder, text.replace(/ {3,}/, split), style, startStyle, endStyle); + }; + } + + function buildCollapsedSpan(builder, size, widget) { + if (widget) { + if (!builder.display) widget = widget.cloneNode(true); + builder.pre.appendChild(widget); + if (builder.measure && size) { + builder.measure[builder.pos] = widget; + builder.addedOne = true; + } + } + builder.pos += size; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans; + if (!spans) { + for (var i = 1; i < styles.length; i+=2) + builder.addToken(builder, styles[i], styleToClass(styles[i+1])); + return; + } + + var allText = line.text, len = allText.length; + var pos = 0, i = 1, text = "", style; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, collapsed; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = ""; + collapsed = null; nextChange = Infinity; + var foundBookmark = null; + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (sp.from <= pos && (sp.to == null || sp.to > pos)) { + if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; } + if (m.className) spanStyle += " " + m.className; + if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; + if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; + if (m.collapsed && (!collapsed || collapsed.marker.width < m.width)) + collapsed = sp; + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + if (m.type == "bookmark" && sp.from == pos && m.replacedWith) + foundBookmark = m.replacedWith; + } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len : collapsed.to) - pos, + collapsed.from != null && collapsed.marker.replacedWith); + if (collapsed.to == null) return collapsed.marker.find(); + } + if (foundBookmark && !collapsed) buildCollapsedSpan(builder, 0, foundBookmark); + } + if (pos >= len) break; + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : ""); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + spanStartStyle = ""; + } + text = styles[i++]; style = styleToClass(styles[i++]); + } + } + } + + // DOCUMENT DATA STRUCTURE + + function updateDoc(doc, change, markedSpans, selAfter, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null;} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // First adjust the line structure + if (from.ch == 0 && to.ch == 0 && lastText == "") { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + for (var i = 0, e = text.length - 1, added = []; i < e; ++i) + added.push(makeLine(text[i], spansFor(i), estimateHeight)); + update(lastLine, lastLine.text, lastSpans); + if (nlines) doc.remove(from.line, nlines); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + for (var added = [], i = 1, e = text.length - 1; i < e; ++i) + added.push(makeLine(text[i], spansFor(i), estimateHeight)); + added.push(makeLine(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + for (var i = 1, e = text.length - 1, added = []; i < e; ++i) + added.push(makeLine(text[i], spansFor(i), estimateHeight)); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1); + doc.insert(from.line + 1, added); + } + + signalLater(doc, "change", doc, change); + setSelection(doc, selAfter.anchor, selAfter.head, null, true); + } + function LeafChunk(lines) { this.lines = lines; this.parent = null; @@ -2706,22 +4375,22 @@ window.CodeMirror = (function() { } this.height = height; } + LeafChunk.prototype = { chunkSize: function() { return this.lines.length; }, - remove: function(at, n, callbacks) { + removeInner: function(at, n) { for (var i = at, e = at + n; i < e; ++i) { var line = this.lines[i]; this.height -= line.height; - line.cleanUp(); - if (line.handlers) - for (var j = 0; j < line.handlers.length; ++j) callbacks.push(line.handlers[j]); + cleanUpLine(line); + signalLater(line, "delete"); } this.lines.splice(at, n); }, collapse: function(lines) { lines.splice.apply(lines, [lines.length, 0].concat(this.lines)); }, - insertHeight: function(at, lines, height) { + insertInner: function(at, lines, height) { this.height += height; this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this; @@ -2731,6 +4400,7 @@ window.CodeMirror = (function() { if (op(this.lines[at])) return true; } }; + function BranchChunk(children) { this.children = children; var size = 0, height = 0; @@ -2743,15 +4413,16 @@ window.CodeMirror = (function() { this.height = height; this.parent = null; } + BranchChunk.prototype = { chunkSize: function() { return this.size; }, - remove: function(at, n, callbacks) { + removeInner: function(at, n) { this.size -= n; for (var i = 0; i < this.children.length; ++i) { var child = this.children[i], sz = child.chunkSize(); if (at < sz) { var rm = Math.min(n, sz - at), oldHeight = child.height; - child.remove(at, rm, callbacks); + child.removeInner(at, rm); this.height -= oldHeight - child.height; if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } if ((n -= rm) == 0) break; @@ -2768,18 +4439,13 @@ window.CodeMirror = (function() { collapse: function(lines) { for (var i = 0, e = this.children.length; i < e; ++i) this.children[i].collapse(lines); }, - insert: function(at, lines) { - var height = 0; - for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height; - this.insertHeight(at, lines, height); - }, - insertHeight: function(at, lines, height) { + insertInner: function(at, lines, height) { this.size += lines.length; this.height += height; for (var i = 0, e = this.children.length; i < e; ++i) { var child = this.children[i], sz = child.chunkSize(); if (at <= sz) { - child.insertHeight(at, lines, height); + child.insertInner(at, lines, height); if (child.lines && child.lines.length > 50) { while (child.lines.length > 50) { var spilled = child.lines.splice(child.lines.length - 25, 25); @@ -2816,7 +4482,6 @@ window.CodeMirror = (function() { } while (me.children.length > 10); me.parent.maybeSpill(); }, - iter: function(from, to, op) { this.iterN(from, to - from, op); }, iterN: function(at, n, op) { for (var i = 0, e = this.children.length; i < e; ++i) { var child = this.children[i], sz = child.chunkSize(); @@ -2830,7 +4495,268 @@ window.CodeMirror = (function() { } }; - function getLineAt(chunk, n) { + var nextDocId = 0; + var Doc = CodeMirror.Doc = function(text, mode, firstLine) { + if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); + if (firstLine == null) firstLine = 0; + + BranchChunk.call(this, [new LeafChunk([makeLine("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.history = makeHistory(); + this.frontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = {from: start, to: start, head: start, anchor: start, shift: false, extend: false, goalColumn: null}; + this.id = ++nextDocId; + this.modeOption = mode; + + if (typeof text == "string") text = splitLines(text); + updateDoc(this, {from: start, to: start, text: text}, null, {head: start, anchor: start}); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + iter: function(from, to, op) { + if (op) this.iterN(from - this.first, to - from, op); + else this.iterN(this.first, this.first + this.size, from); + }, + + insert: function(at, lines) { + var height = 0; + for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height; + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + setValue: function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: splitLines(code), origin: "setValue"}, + {head: top, anchor: top}, true); + }, + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, + setLine: function(line, text) { + if (isLine(this, line)) + replaceRange(this, text, Pos(line, 0), clipPos(this, Pos(line))); + }, + removeLine: function(line) { + if (line) replaceRange(this, "", clipPos(this, Pos(line - 1)), clipPos(this, Pos(line))); + else replaceRange(this, "", Pos(0, 0), clipPos(this, Pos(1, 0))); + }, + + getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, + getLineNumber: function(line) {return lineNo(line);}, + + lineCount: function() {return this.size;}, + firstLine: function() {return this.first;}, + lastLine: function() {return this.first + this.size - 1;}, + + clipPos: function(pos) {return clipPos(this, pos);}, + + getCursor: function(start) { + var sel = this.sel, pos; + if (start == null || start == "head") pos = sel.head; + else if (start == "anchor") pos = sel.anchor; + else if (start == "end" || start === false) pos = sel.to; + else pos = sel.from; + return copyPos(pos); + }, + somethingSelected: function() {return !posEq(this.sel.head, this.sel.anchor);}, + + setCursor: docOperation(function(line, ch, extend) { + var pos = clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line); + if (extend) extendSelection(this, pos); + else setSelection(this, pos, pos); + }), + setSelection: docOperation(function(anchor, head) { + setSelection(this, clipPos(this, anchor), clipPos(this, head || anchor)); + }), + extendSelection: docOperation(function(from, to) { + extendSelection(this, clipPos(this, from), to && clipPos(this, to)); + }), + + getSelection: function(lineSep) {return this.getRange(this.sel.from, this.sel.to, lineSep);}, + replaceSelection: function(code, collapse, origin) { + makeChange(this, {from: this.sel.from, to: this.sel.to, text: splitLines(code), origin: origin}, collapse || "around"); + }, + undo: docOperation(function() {makeChangeFromHistory(this, "undo");}), + redo: docOperation(function() {makeChangeFromHistory(this, "redo");}), + + setExtending: function(val) {this.sel.extend = val;}, + + historySize: function() { + var hist = this.history; + return {undo: hist.done.length, redo: hist.undone.length}; + }, + clearHistory: function() {this.history = makeHistory();}, + + markClean: function() { + this.history.dirtyCounter = 0; + this.history.lastOp = this.history.lastOrigin = null; + }, + isClean: function () {return this.history.dirtyCounter == 0;}, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)}; + }, + setHistory: function(histData) { + var hist = this.history = makeHistory(); + hist.done = histData.done.slice(0); + hist.undone = histData.undone.slice(0); + }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark"); + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + markers.push(span.marker.parent || span.marker); + } + return markers; + }, + getAllMarks: function() { + var markers = []; + this.iter(function(line) { + var sps = line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) + if (sps[i].from != null) markers.push(sps[i].marker); + }); + return markers; + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first; + this.iter(function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)); + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) return 0; + this.iter(this.first, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = {from: this.sel.from, to: this.sel.to, head: this.sel.head, anchor: this.sel.anchor, + shift: this.sel.shift, extend: false, goalColumn: this.sel.goalColumn}; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc; + }, + + linkedDoc: function(options) { + if (!options) options = {}; + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) from = options.from; + if (options.to != null && options.to < to) to = options.to; + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); + if (options.sharedHist) copy.history = this.history; + (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + return copy; + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) other = other.doc; + if (this.linked) for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) continue; + this.linked.splice(i, 1); + other.unlinkDoc(this); + break; + } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); + other.history = makeHistory(); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode;}, + getEditor: function() {return this.cm;} + }); + + Doc.prototype.eachLine = Doc.prototype.iter; + + // The Doc methods that should be available on CodeMirror instances + var dontDelegate = "iter insert remove copy getEditor".split(" "); + for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments);}; + })(Doc.prototype[prop]); + + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) continue; + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) continue; + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } + } + propagate(doc, null, true); + } + + function attachDoc(cm, doc) { + if (doc.cm) throw new Error("This document is already in use."); + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + if (!cm.options.lineWrapping) computeMaxLength(cm); + cm.options.mode = doc.modeOption; + regChange(cm); + } + + // LINE UTILITIES + + function getLine(chunk, n) { + n -= chunk.first; while (!chunk.lines) { for (var i = 0;; ++i) { var child = chunk.children[i], sz = child.chunkSize(); @@ -2840,19 +4766,43 @@ window.CodeMirror = (function() { } return chunk.lines[n]; } + + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function(line) { + var text = line.text; + if (n == end.line) text = text.slice(0, end.ch); + if (n == start.line) text = text.slice(start.ch); + out.push(text); + ++n; + }); + return out; + } + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function(line) { out.push(line.text); }); + return out; + } + + function updateLineHeight(line, height) { + var diff = height - line.height; + for (var n = line; n; n = n.parent) n.height += diff; + } + function lineNo(line) { if (line.parent == null) return null; var cur = line.parent, no = indexOf(cur.lines, line); for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { - for (var i = 0, e = chunk.children.length; ; ++i) { + for (var i = 0;; ++i) { if (chunk.children[i] == cur) break; no += chunk.children[i].chunkSize(); } } - return no; - } + return no + cur.first; + } + function lineAtHeight(chunk, h) { - var n = 0; + var n = chunk.first; outer: do { for (var i = 0, e = chunk.children.length; i < e; ++i) { var child = chunk.children[i], ch = child.height; @@ -2869,58 +4819,197 @@ window.CodeMirror = (function() { } return n + i; } - function heightAtLine(chunk, n) { - var h = 0; - outer: do { - for (var i = 0, e = chunk.children.length; i < e; ++i) { - var child = chunk.children[i], sz = child.chunkSize(); - if (n < sz) { chunk = child; continue outer; } - n -= sz; - h += child.height; + + function heightAtLine(cm, lineObj) { + lineObj = visualLine(cm.doc, lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) break; + else h += line.height; + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i = 0; i < p.children.length; ++i) { + var cur = p.children[i]; + if (cur == chunk) break; + else h += cur.height; } - return h; - } while (!chunk.lines); - for (var i = 0; i < n; ++i) h += chunk.lines[i].height; + } return h; } - // The history object 'chunks' changes that are made close together - // and at almost the same time into bigger undoable units. - function History() { - this.time = 0; - this.done = []; this.undone = []; - this.compound = 0; - this.closed = false; - } - History.prototype = { - addChange: function(start, added, old) { - this.undone.length = 0; - var time = +new Date, cur = lst(this.done), last = cur && lst(cur); - var dtime = time - this.time; - - if (cur && !this.closed && this.compound) { - cur.push({start: start, added: added, old: old}); - } else if (dtime > 400 || !last || this.closed || - last.start > start + old.length || last.start + last.added < start) { - this.done.push([{start: start, added: added, old: old}]); - this.closed = false; + function getOrder(line) { + var order = line.order; + if (order == null) order = line.order = bidiOrdering(line.text); + return order; + } + + // HISTORY + + function makeHistory() { + return { + // Arrays of history events. Doing something adds an event to + // done and clears undo. Undoing moves events from done to + // undone, redoing moves them in the other direction. + done: [], undone: [], undoDepth: Infinity, + // Used to track when changes can be merged into a single undo + // event + lastTime: 0, lastOp: null, lastOrigin: null, + // Used by the isClean() method + dirtyCounter: 0 + }; + } + + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { + if (line.markedSpans) + (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; + ++n; + }); + } + + function historyChangeFromChange(doc, change) { + var histChange = {from: change.from, to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); + return histChange; + } + + function addToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur = lst(hist.done); + + if (cur && + (hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && doc.cm && hist.lastTime > time - doc.cm.options.historyEventDelay) || + change.origin.charAt(0) == "*"))) { + // Merge this change into the last event + var last = lst(cur.changes); + if (posEq(change.from, change.to) && posEq(change.from, last.to)) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); } else { - var startBefore = Math.max(0, last.start - start), - endAfter = Math.max(0, (start + old.length) - (last.start + last.added)); - for (var i = startBefore; i > 0; --i) last.old.unshift(old[i - 1]); - for (var i = endAfter; i > 0; --i) last.old.push(old[old.length - i]); - if (startBefore) last.start = start; - last.added += added - (old.length - startBefore - endAfter); + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + cur.anchorAfter = selAfter.anchor; cur.headAfter = selAfter.head; + } else { + // Can not be merged, start a new event. + cur = {changes: [historyChangeFromChange(doc, change)], + anchorBefore: doc.sel.anchor, headBefore: doc.sel.head, + anchorAfter: selAfter.anchor, headAfter: selAfter.head}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) + hist.done.shift(); + if (hist.dirtyCounter < 0) + // The user has made a change after undoing past the last clean state. + // We can never get back to a clean state now until markClean() is called. + hist.dirtyCounter = NaN; + else + hist.dirtyCounter++; + } + hist.lastTime = time; + hist.lastOp = opId; + hist.lastOrigin = change.origin; + } + + function removeClearedSpans(spans) { + if (!spans) return null; + for (var i = 0, out; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } + else if (out) out.push(spans[i]); + } + return !out ? spans : out.length ? out : null; + } + + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) return null; + for (var i = 0, nw = []; i < change.text.length; ++i) + nw.push(removeClearedSpans(found[i])); + return nw; + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup) { + for (var i = 0, copy = []; i < events.length; ++i) { + var event = events[i], changes = event.changes, newChanges = []; + copy.push({changes: newChanges, anchorBefore: event.anchorBefore, headBefore: event.headBefore, + anchorAfter: event.anchorAfter, headAfter: event.headAfter}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m; + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } } - this.time = time; - }, - startCompound: function() { - if (!this.compound++) this.closed = true; - }, - endCompound: function() { - if (!--this.compound) this.closed = true; - } - }; + } + return copy; + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSel(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + for (var j = 0; j < sub.changes.length; ++j) { + var cur = sub.changes[j]; + if (!sub.copied) { cur.from = copyPos(cur.from); cur.to = copyPos(cur.to); } + if (to < cur.from.line) { + cur.from.line += diff; + cur.to.line += diff; + } else if (from <= cur.to.line) { + ok = false; + break; + } + } + if (!sub.copied) { + sub.anchorBefore = copyPos(sub.anchorBefore); sub.headBefore = copyPos(sub.headBefore); + sub.anchorAfter = copyPos(sub.anchorAfter); sub.readAfter = copyPos(sub.headAfter); + sub.copied = true; + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } else { + rebaseHistSel(sub.anchorBefore); rebaseHistSel(sub.headBefore); + rebaseHistSel(sub.anchorAfter); rebaseHistSel(sub.headAfter); + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // EVENT OPERATORS function stopMethod() {e_stop(this);} // Ensure an event has a stop method. @@ -2954,96 +5043,95 @@ window.CodeMirror = (function() { return b; } - // Allow 3rd-party code to override event properties by adding an override - // object to an event object. - function e_prop(e, prop) { - var overridden = e.override && e.override.hasOwnProperty(prop); - return overridden ? e.override[prop] : e[prop]; - } - - // Event handler registration. If disconnect is true, it'll return a - // function that unregisters the handler. - function connect(node, type, handler, disconnect) { - if (typeof node.addEventListener == "function") { - node.addEventListener(type, handler, false); - if (disconnect) return function() {node.removeEventListener(type, handler, false);}; - } else { - var wrapHandler = function(event) {handler(event || window.event);}; - node.attachEvent("on" + type, wrapHandler); - if (disconnect) return function() {node.detachEvent("on" + type, wrapHandler);}; - } - } - CodeMirror.connect = connect; + // EVENT HANDLING + + function on(emitter, type, f) { + if (emitter.addEventListener) + emitter.addEventListener(type, f, false); + else if (emitter.attachEvent) + emitter.attachEvent("on" + type, f); + else { + var map = emitter._handlers || (emitter._handlers = {}); + var arr = map[type] || (map[type] = []); + arr.push(f); + } + } + + function off(emitter, type, f) { + if (emitter.removeEventListener) + emitter.removeEventListener(type, f, false); + else if (emitter.detachEvent) + emitter.detachEvent("on" + type, f); + else { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + for (var i = 0; i < arr.length; ++i) + if (arr[i] == f) { arr.splice(i, 1); break; } + } + } + + function signal(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); + } + + var delayedCallbacks, delayedCallbackDepth = 0; + function signalLater(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + if (!delayedCallbacks) { + ++delayedCallbackDepth; + delayedCallbacks = []; + setTimeout(fireDelayed, 0); + } + function bnd(f) {return function(){f.apply(null, args);};}; + for (var i = 0; i < arr.length; ++i) + delayedCallbacks.push(bnd(arr[i])); + } + + function fireDelayed() { + --delayedCallbackDepth; + var delayed = delayedCallbacks; + delayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + function hasHandler(emitter, type) { + var arr = emitter._handlers && emitter._handlers[type]; + return arr && arr.length > 0; + } + + CodeMirror.on = on; CodeMirror.off = off; CodeMirror.signal = signal; + + // MISC UTILITIES + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerCutOff = 30; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; function Delayed() {this.id = null;} Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}}; - var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; - - // Detect drag-and-drop - var dragAndDrop = function() { - // There is *some* kind of drag-and-drop support in IE6-8, but I - // couldn't get it to work yet. - if (ie_lt9) return false; - var div = elt('div'); - return "draggable" in div || "dragDrop" in div; - }(); - - // Feature-detect whether newlines in textareas are converted to \r\n - var lineSep = function () { - var te = elt("textarea"); - te.value = "foo\nbar"; - if (te.value.indexOf("\r") > -1) return "\r\n"; - return "\n"; - }(); - - // For a reason I have yet to figure out, some browsers disallow - // word wrapping between certain characters *only* if a new inline - // element is started between them. This makes it hard to reliably - // measure the position of things, since that requires inserting an - // extra span. This terribly fragile set of regexps matches the - // character combinations that suffer from this phenomenon on the - // various browsers. - var spanAffectsWrapping = /^$/; // Won't match any two-character string - if (gecko) spanAffectsWrapping = /$'/; - else if (safari) spanAffectsWrapping = /\-[^ \-?]|\?[^ !'\"\),.\-\/:;\?\]\}]/; - else if (chrome) spanAffectsWrapping = /\-[^ \-\.?]|\?[^ \-\.?\]\}:;!'\"\),\/]|[\.!\"#&%\)*+,:;=>\]|\}~][\(\{\[<]|\$'/; - // Counts the column offset in a string, taking tabs into account. // Used mostly to find indentation. - function countColumn(string, end, tabSize) { + function countColumn(string, end, tabSize, startIndex, startValue) { if (end == null) { end = string.search(/[^\s\u00a0]/); if (end == -1) end = string.length; } - for (var i = 0, n = 0; i < end; ++i) { + for (var i = startIndex || 0, n = startValue || 0; i < end; ++i) { if (string.charAt(i) == "\t") n += tabSize - (n % tabSize); else ++n; } return n; } - - function eltOffset(node, screen) { - // Take the parts of bounding client rect that we are interested in so we are able to edit if need be, - // since the returned value cannot be changed externally (they are kept in sync as the element moves within the page) - try { var box = node.getBoundingClientRect(); box = { top: box.top, left: box.left }; } - catch(e) { box = {top: 0, left: 0}; } - if (!screen) { - // Get the toplevel scroll, working around browser differences. - if (window.pageYOffset == null) { - var t = document.documentElement || document.body.parentNode; - if (t.scrollTop == null) t = document.body; - box.top += t.scrollTop; box.left += t.scrollLeft; - } else { - box.top += window.pageYOffset; box.left += window.pageXOffset; - } - } - return box; - } - - function eltText(node) { - return node.textContent || node.innerText || node.nodeValue || ""; - } + CodeMirror.countColumn = countColumn; var spaceStrs = [""]; function spaceStr(n) { @@ -3061,10 +5149,51 @@ window.CodeMirror = (function() { } else node.select(); } - // Operations on {line, ch} objects. - function posEq(a, b) {return a.line == b.line && a.ch == b.ch;} - 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};} + function indexOf(collection, elt) { + if (collection.indexOf) return collection.indexOf(elt); + for (var i = 0, e = collection.length; i < e; ++i) + if (collection[i] == elt) return i; + return -1; + } + + function createObj(base, props) { + function Obj() {} + Obj.prototype = base; + var inst = new Obj(); + if (props) copyObj(props, inst); + return inst; + } + + function copyObj(obj, target) { + if (!target) target = {}; + for (var prop in obj) if (obj.hasOwnProperty(prop)) target[prop] = obj[prop]; + return target; + } + + function emptyArray(size) { + for (var a = [], i = 0; i < size; ++i) a.push(undefined); + return a; + } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args);}; + } + + var nonASCIISingleCaseWordChar = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc]/; + function isWordChar(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); + } + + function isEmpty(obj) { + for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; + return true; + } + + var isExtendingChar = /[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\uA66F\uA670-\uA672\uA674-\uA67D\uA69F\udc00-\udfff]/; + + // DOM UTILITIES function elt(tag, content, className, style) { var e = document.createElement(tag); @@ -3074,13 +5203,17 @@ window.CodeMirror = (function() { else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); return e; } + function removeChildren(e) { - e.innerHTML = ""; + for (var count = e.childNodes.length; count > 0; --count) + e.removeChild(e.firstChild); return e; } + function removeChildrenAndAdd(parent, e) { - removeChildren(parent).appendChild(e); - } + return removeChildren(parent).appendChild(e); + } + function setTextContent(e, str) { if (ie_lt9) { e.innerHTML = ""; @@ -3088,26 +5221,54 @@ window.CodeMirror = (function() { } else e.textContent = str; } - // Used to position the cursor after an undo/redo by finding the - // last edited character. - function editEnd(from, to) { - if (!to) return 0; - if (!from) return to.length; - for (var i = from.length, j = to.length; i >= 0 && j >= 0; --i, --j) - if (from.charAt(i) != to.charAt(j)) break; - return j + 1; - } - - function indexOf(collection, elt) { - if (collection.indexOf) return collection.indexOf(elt); - for (var i = 0, e = collection.length; i < e; ++i) - if (collection[i] == elt) return i; - return -1; - } - var nonASCIISingleCaseWordChar = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc]/; - function isWordChar(ch) { - return /\w/.test(ch) || ch > "\x80" && - (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); + function getRect(node) { + return node.getBoundingClientRect(); + } + CodeMirror.replaceGetRect = function(f) { getRect = f; }; + + // FEATURE DETECTION + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie_lt9) return false; + var div = elt('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + // For a reason I have yet to figure out, some browsers disallow + // word wrapping between certain characters *only* if a new inline + // element is started between them. This makes it hard to reliably + // measure the position of things, since that requires inserting an + // extra span. This terribly fragile set of regexps matches the + // character combinations that suffer from this phenomenon on the + // various browsers. + var spanAffectsWrapping = /^$/; // Won't match any two-character string + if (gecko) spanAffectsWrapping = /$'/; + else if (safari && !/Version\/([6-9]|\d\d)\b/.test(navigator.userAgent)) spanAffectsWrapping = /\-[^ \-?]|\?[^ !'\"\),.\-\/:;\?\]\}]/; + else if (webkit) spanAffectsWrapping = /[~!#%&*)=+}\]|\"\.>,:;][({[<]|-[^\-?\.]|\?[\w~`@#$%\^&*(_=+{[|><]/; + + var knownScrollbarWidth; + function scrollbarWidth(measure) { + if (knownScrollbarWidth != null) return knownScrollbarWidth; + var test = elt("div", null, null, "width: 50px; height: 50px; overflow-x: scroll"); + removeChildrenAndAdd(measure, test); + if (test.offsetWidth) + knownScrollbarWidth = test.offsetHeight - test.clientHeight; + return knownScrollbarWidth || 0; + } + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !ie_lt8; + } + if (zwspSupported) return elt("span", "\u200b"); + else return elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); } // See if "".split is the broken IE version, if so, provide an @@ -3141,10 +5302,14 @@ window.CodeMirror = (function() { return range.compareEndPoints("StartToEnd", range) != 0; }; - CodeMirror.defineMode("null", function() { - return {token: function(stream) {stream.skipToEnd();}}; - }); - CodeMirror.defineMIME("text/plain", "null"); + var hasCopyEvent = (function() { + var e = elt("div"); + if ("oncopy" in e) return true; + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == 'function'; + })(); + + // KEY NAMING var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", @@ -3163,7 +5328,256 @@ window.CodeMirror = (function() { for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; })(); - CodeMirror.version = "2.36"; + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) return f(from, to, "ltr"); + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); + } + } + + function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } + function bidiRight(part) { return part.level % 2 ? part.from : part.to; } + + function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } + function lineRight(line) { + var order = getOrder(line); + if (!order) return line.text.length; + return bidiRight(lst(order)); + } + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(cm.doc, line); + if (visual != line) lineN = lineNo(visual); + var order = getOrder(visual); + var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); + return Pos(lineN, ch); + } + function lineEnd(cm, lineN) { + var merged, line; + while (merged = collapsedSpanAtEnd(line = getLine(cm.doc, lineN))) + lineN = merged.find().to.line; + var order = getOrder(line); + var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); + return Pos(lineN, ch); + } + + // This is somewhat involved. It is needed in order to move + // 'visually' through bi-directional text -- i.e., pressing left + // should make the cursor go left, even when in RTL text. The + // tricky part is the 'jumps', where RTL and LTR text touch each + // other. This often requires the cursor offset to move more than + // one unit, in order to visually move one unit. + function moveVisually(line, start, dir, byUnit) { + var bidi = getOrder(line); + if (!bidi) return moveLogically(line, start, dir, byUnit); + var moveOneUnit = byUnit ? function(pos, dir) { + do pos += dir; + while (pos > 0 && isExtendingChar.test(line.text.charAt(pos))); + return pos; + } : function(pos, dir) { return pos + dir; }; + var linedir = bidi[0].level; + for (var i = 0; i < bidi.length; ++i) { + var part = bidi[i], sticky = part.level % 2 == linedir; + if ((part.from < start && part.to > start) || + (sticky && (part.from == start || part.to == start))) break; + } + var target = moveOneUnit(start, part.level % 2 ? -dir : dir); + + while (target != null) { + if (part.level % 2 == linedir) { + if (target < part.from || target > part.to) { + part = bidi[i += dir]; + target = part && (dir > 0 == part.level % 2 ? moveOneUnit(part.to, -1) : moveOneUnit(part.from, 1)); + } else break; + } else { + if (target == bidiLeft(part)) { + part = bidi[--i]; + target = part && bidiRight(part); + } else if (target == bidiRight(part)) { + part = bidi[++i]; + target = part && bidiLeft(part); + } else break; + } + } + + return target < 0 || target > line.text.length ? null : target; + } + + function moveLogically(line, start, dir, byUnit) { + var target = start + dir; + if (byUnit) while (target > 0 && isExtendingChar.test(line.text.charAt(target))) target += dir; + return target < 0 || target > line.text.length ? null : target; + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLL"; + // Character types for codepoints 0x600 to 0x6ff + var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmmrrrrrrrrrrrrrrrrrr"; + function charType(code) { + if (code <= 0xff) return lowTypes.charAt(code); + else if (0x590 <= code && code <= 0x5f4) return "R"; + else if (0x600 <= code && code <= 0x6ff) return arabicTypes.charAt(code - 0x600); + else if (0x700 <= code && code <= 0x8ac) return "r"; + else return "L"; + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + // Browsers seem to always treat the boundaries of block elements as being L. + var outerType = "L"; + + return function(str) { + if (!bidiRE.test(str)) return false; + var len = str.length, types = []; + for (var i = 0, type; i < len; ++i) + types.push(type = charType(str.charCodeAt(i))); + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i = 0, prev = outerType; i < len; ++i) { + var type = types[i]; + if (type == "m") types[i] = prev; + else prev = type; + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (type == "1" && cur == "r") types[i] = "n"; + else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i = 1, prev = types[0]; i < len - 1; ++i) { + var type = types[i]; + if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; + else if (type == "," && prev == types[i+1] && + (prev == "1" || prev == "n")) types[i] = prev; + prev = type; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i = 0; i < len; ++i) { + var type = types[i]; + if (type == ",") types[i] = "N"; + else if (type == "%") { + for (var end = i + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i && types[i-1] == "!") || (end < len - 1 && types[end] == "1") ? "1" : "N"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (cur == "L" && type == "1") types[i] = "L"; + else if (isStrong.test(type)) cur = type; + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i = 0; i < len; ++i) { + if (isNeutral.test(types[i])) { + for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} + var before = (i ? types[i-1] : outerType) == "L"; + var after = (end < len - 1 ? types[end] : outerType) == "L"; + var replace = before || after ? "L" : "R"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i = 0; i < len;) { + if (countsAsLeft.test(types[i])) { + var start = i; + for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} + order.push({from: start, to: i, level: 0}); + } else { + var pos = i, at = order.length; + for (++i; i < len && types[i] != "L"; ++i) {} + for (var j = pos; j < i;) { + if (countsAsNum.test(types[j])) { + if (pos < j) order.splice(at, 0, {from: pos, to: j, level: 1}); + var nstart = j; + for (++j; j < i && countsAsNum.test(types[j]); ++j) {} + order.splice(at, 0, {from: nstart, to: j, level: 2}); + pos = j; + } else ++j; + } + if (pos < i) order.splice(at, 0, {from: pos, to: i, level: 1}); + } + } + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift({from: 0, to: m[0].length, level: 0}); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push({from: len - m[0].length, to: len, level: 0}); + } + if (order[0].level != lst(order).level) + order.push({from: len, to: len, level: order[0].level}); + + return order; + }; + })(); + + // THE END + + CodeMirror.version = "3.12"; return CodeMirror; })();