From cfa68dade50e2d79d42a6414ca19189e5562b1fd Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 25 Oct 2013 09:48:57 +0100 Subject: [PATCH] Update CodeMirror plugin to latest version and the new widget framework Now the CodeMirror plugin modifies the behaviour of the core edit-text widget. --- .../tiddlywiki/codemirror/codemirroreditor.js | 118 +- .../codemirror/files/codemirror.css | 56 +- .../tiddlywiki/codemirror/files/codemirror.js | 1714 +++++++++++------ plugins/tiddlywiki/codemirror/styles.tid | 1 + 4 files changed, 1190 insertions(+), 699 deletions(-) diff --git a/plugins/tiddlywiki/codemirror/codemirroreditor.js b/plugins/tiddlywiki/codemirror/codemirroreditor.js index 7b88ddec5..08ae8d71c 100644 --- a/plugins/tiddlywiki/codemirror/codemirroreditor.js +++ b/plugins/tiddlywiki/codemirror/codemirroreditor.js @@ -1,9 +1,9 @@ /*\ -title: $:/plugins/tiddlywiki/codemirror/codemirroreditor.js +title: $:/core/modules/new_widgets/edit-text-codemirror.js type: application/javascript -module-type: editor +module-type: new_widget -A Codemirror text editor +Extend the edit-text widget to use CodeMirror \*/ (function(){ @@ -13,100 +13,40 @@ A Codemirror text editor "use strict"; if($tw.browser) { - require("./codemirror.js") + require("$:/plugins/tiddlywiki/codemirror/codemirror.js"); } -var CodeMirrorEditor = function(editWidget,tiddlerTitle,fieldName) { - this.editWidget = editWidget; - this.tiddlerTitle = tiddlerTitle; - this.fieldName = fieldName; -}; +var EditTextWidget = require("$:/core/modules/new_widgets/edit-text.js")["edit-text"]; /* -Get the tiddler being edited and current value +The edit-text widget calls this method just after inserting its dom nodes */ -CodeMirrorEditor.prototype.getEditInfo = function() { - // Get the current tiddler and the field name - var tiddler = this.editWidget.renderer.renderTree.wiki.getTiddler(this.tiddlerTitle), - value; - // If we've got a tiddler, the value to display is the field string value - if(tiddler) { - value = tiddler.getFieldString(this.fieldName); +EditTextWidget.prototype.postRender = function() { + var self = this, + cm; + if($tw.browser && window.CodeMirror && this.editTag === "textarea") { + cm = CodeMirror.fromTextArea(this.domNodes[0],{ + lineWrapping: true, + lineNumbers: true + }); + cm.on("change",function() { + self.saveChanges(cm.getValue()); + }); } else { - // Otherwise, we need to construct a default value for the editor - switch(this.fieldName) { - case "text": - value = "Type the text for the tiddler '" + this.tiddlerTitle + "'"; - break; - case "title": - value = this.tiddlerTitle; - break; - default: - value = ""; - break; + cm = undefined; + } + this.codemirrorInstance = cm; +}; + +EditTextWidget.prototype.updateEditor = function(text) { + // Replace the edit value if the tiddler we're editing has changed + if(this.codemirrorInstance) { + if(!this.codemirrorInstance.hasFocus()) { + this.codemirrorInstance.setValue(text); } - value = this.editWidget.renderer.getAttribute("default",value); - } - return {tiddler: tiddler, value: value}; -}; - -CodeMirrorEditor.prototype.render = function() { - // Get the initial value of the editor - var editInfo = this.getEditInfo(); - // Create the editor nodes - var node = { - type: "element", - attributes: {} - }; - this.type = this.editWidget.renderer.getAttribute("type",this.fieldName === "text" ? "textarea" : "input"); - switch(this.type) { - case "textarea": - node.tag = "textarea"; - node.children = [{ - type: "text", - text: editInfo.value - }]; - break; - case "search": - node.tag = "input"; - node.attributes.type = {type: "string", value: "search"}; - node.attributes.value = {type: "string", value: editInfo.value}; - break; - default: // "input" - node.tag = "input"; - node.attributes.type = {type: "string", value: "text"}; - node.attributes.value = {type: "string", value: editInfo.value}; - break; - } - // Set the element details - this.editWidget.tag = this.editWidget.renderer.parseTreeNode.isBlock ? "div" : "span"; - this.editWidget.attributes = { - "class": "tw-edit-CodeMirrorEditor" - }; - this.editWidget.children = this.editWidget.renderer.renderTree.createRenderers(this.editWidget.renderer,[node]); -}; - -CodeMirrorEditor.prototype.postRenderInDom = function() { - if(this.type === "textarea") { - var self = this; - // HACK: We use the timeout because postRenderInDom is called before the dom nodes have been added to the document - window.setTimeout(function() { - self.codemirrorInstance = CodeMirror.fromTextArea(self.editWidget.children[0].domNode,{ - lineWrapping: true, - lineNumbers: true - }); - },1); + } else { + this.updateEditorDomNode(); } }; -CodeMirrorEditor.prototype.refreshInDom = function() { - if(document.activeElement !== this.editWidget.children[0].domNode) { - var editInfo = this.getEditInfo(); - this.editWidget.children[0].domNode.value = editInfo.value; - } -}; - -exports["text/vnd.tiddlywiki"] = CodeMirrorEditor; -exports["text/plain"] = CodeMirrorEditor; - })(); diff --git a/plugins/tiddlywiki/codemirror/files/codemirror.css b/plugins/tiddlywiki/codemirror/files/codemirror.css index 8de0b1908..23eaf74d4 100644 --- a/plugins/tiddlywiki/codemirror/files/codemirror.css +++ b/plugins/tiddlywiki/codemirror/files/codemirror.css @@ -19,7 +19,7 @@ padding: 0 4px; /* Horizontal padding of content */ } -.CodeMirror-scrollbar-filler { +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { background-color: white; /* The little square between H and V scrollbars */ } @@ -28,6 +28,7 @@ .CodeMirror-gutters { border-right: 1px solid #ddd; background-color: #f7f7f7; + white-space: nowrap; } .CodeMirror-linenumbers {} .CodeMirror-linenumber { @@ -41,6 +42,7 @@ .CodeMirror div.CodeMirror-cursor { border-left: 1px solid black; + z-index: 3; } /* Shown when moving in bi-directional text */ .CodeMirror div.CodeMirror-secondarycursor { @@ -49,17 +51,14 @@ .CodeMirror.cm-keymap-fat-cursor div.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 */ -.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor:not(#nonsense_id) { - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + 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;} @@ -75,7 +74,6 @@ .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;} @@ -90,13 +88,14 @@ .cm-positive {color: #292;} .cm-header, .cm-strong {font-weight: bold;} .cm-em {font-style: italic;} -.cm-emstrong {font-style: italic; font-weight: bold;} .cm-link {text-decoration: underline;} +.cm-s-default .cm-error {color: #f00;} .cm-invalidchar {color: #f00;} div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-activeline-background {background: #e8f2ff;} /* STOP */ @@ -107,16 +106,20 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} line-height: 1; position: relative; overflow: hidden; + background: white; + color: black; } .CodeMirror-scroll { /* 30px is the magic margin used to hide the element's real scrollbars */ - /* See overflow: hidden in .CodeMirror, and the paddings in .CodeMirror-sizer */ + /* 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; + -moz-box-sizing: content-box; + box-sizing: content-box; } .CodeMirror-sizer { position: relative; @@ -125,7 +128,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} /* 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 { +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { position: absolute; z-index: 6; display: none; @@ -142,17 +145,23 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} } .CodeMirror-scrollbar-filler { right: 0; bottom: 0; - z-index: 6; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; } .CodeMirror-gutters { position: absolute; left: 0; top: 0; - height: 100%; padding-bottom: 30px; z-index: 3; } .CodeMirror-gutter { + white-space: normal; height: 100%; + -moz-box-sizing: content-box; + box-sizing: content-box; + padding-bottom: 30px; + margin-bottom: -32px; display: inline-block; /* Hack to make IE7 behave */ *zoom:1; @@ -169,7 +178,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} } .CodeMirror pre { /* Reset some styles that the rest of the page might have set */ - -moz-border-radius: 0; -webkit-border-radius: 0; -o-border-radius: 0; border-radius: 0; + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; border-width: 0; background: transparent; font-family: inherit; @@ -188,6 +197,16 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} white-space: pre-wrap; word-break: normal; } +.CodeMirror-code pre { + border-right: 30px solid transparent; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} +.CodeMirror-wrap .CodeMirror-code pre { + border-right: none; + width: auto; +} .CodeMirror-linebackground { position: absolute; left: 0; right: 0; top: 0; bottom: 0; @@ -200,9 +219,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} overflow: auto; } -.CodeMirror-widget { - display: inline-block; -} +.CodeMirror-widget {} .CodeMirror-wrap .CodeMirror-scroll { overflow-x: hidden; @@ -210,7 +227,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} .CodeMirror-measure { position: absolute; - width: 100%; height: 0px; + width: 100%; + height: 0; overflow: hidden; visibility: hidden; } diff --git a/plugins/tiddlywiki/codemirror/files/codemirror.js b/plugins/tiddlywiki/codemirror/files/codemirror.js index 87339d05f..9f35191bf 100644 --- a/plugins/tiddlywiki/codemirror/files/codemirror.js +++ b/plugins/tiddlywiki/codemirror/files/codemirror.js @@ -1,4 +1,4 @@ -// CodeMirror version 3.1 +// CodeMirror version 3.19.1 // // CodeMirror is the only global var we claim window.CodeMirror = (function() { @@ -26,10 +26,11 @@ window.CodeMirror = (function() { // 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 windows = /win/i.test(navigator.platform); var opera_version = opera && navigator.userAgent.match(/Version\/(\d*\.\d*)/); if (opera_version) opera_version = Number(opera_version[1]); + if (opera_version && opera_version >= 15) { opera = false; webkit = true; } // 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); @@ -41,7 +42,7 @@ window.CodeMirror = (function() { 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. for (var opt in defaults) if (!options.hasOwnProperty(opt) && defaults.hasOwnProperty(opt)) @@ -93,18 +94,23 @@ window.CodeMirror = (function() { function makeDisplay(place, docStart) { var d = {}; - var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none;"); + + 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"); - input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "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"); input.setAttribute("spellcheck", "false"); + // Wraps and hides input textarea 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"); + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); // DIVs containing the selection and the actual code - d.lineDiv = elt("div"); + d.lineDiv = elt("div", null, "CodeMirror-code"); 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 d.cursor = elt("div", "\u00a0", "CodeMirror-cursor"); @@ -120,18 +126,16 @@ window.CodeMirror = (function() { // Set to the height of the text, causes scrolling 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", "\u00a0", null, "position: absolute; height: " + scrollerCutOff + "px"); + 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 - d.scroller = elt("div", [scrollerInner], "CodeMirror-scroll"); + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); d.scroller.setAttribute("tabIndex", "-1"); // The element in which the editor lives. d.wrapper = elt("div", [d.inputDiv, d.scrollbarH, d.scrollbarV, - d.scrollbarFiller, d.scroller], "CodeMirror"); + d.scrollbarFiller, d.gutterFiller, 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); @@ -162,8 +166,6 @@ window.CodeMirror = (function() { d.pollingFast = false; // Self-resetting timeout for the poller d.poll = new Delayed(); - // True when a drag from the editor is active - d.draggingText = false; d.cachedCharWidth = d.cachedTextHeight = null; d.measureLineCache = []; @@ -181,7 +183,7 @@ window.CodeMirror = (function() { // Used for measuring wheel scrolling granularity d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; - + return d; } @@ -212,7 +214,7 @@ window.CodeMirror = (function() { estimateLineHeights(cm); regChange(cm); clearCaches(cm); - setTimeout(function(){updateScrollbars(cm.display, cm.doc.height);}, 100); + setTimeout(function(){updateScrollbars(cm);}, 100); } function estimateHeight(cm) { @@ -237,9 +239,10 @@ window.CodeMirror = (function() { } function keyMapChanged(cm) { - var style = keyMap[cm.options.keyMap].style; + var map = keyMap[cm.options.keyMap], style = map.style; cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-keymap-\S+/g, "") + (style ? " cm-keymap-" + style : ""); + cm.state.disableInput = map.disableInput; } function themeChanged(cm) { @@ -251,6 +254,7 @@ window.CodeMirror = (function() { function guttersChanged(cm) { updateGutters(cm); regChange(cm); + setTimeout(function(){alignHorizontally(cm);}, 20); } function updateGutters(cm) { @@ -302,46 +306,59 @@ window.CodeMirror = (function() { // 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); - } + var found = indexOf(options.gutters, "CodeMirror-linenumbers"); + if (found == -1 && options.lineNumbers) { + options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); + } else if (found > -1 && !options.lineNumbers) { + options.gutters = options.gutters.slice(0); + options.gutters.splice(found, 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 + 2 * paddingTop(d); + function updateScrollbars(cm) { + var d = cm.display, docHeight = cm.doc.height; + var totalHeight = docHeight + paddingVert(d); d.sizer.style.minHeight = d.heightForcer.style.top = totalHeight + "px"; + d.gutters.style.height = Math.max(totalHeight, d.scroller.clientHeight - scrollerCutOff) + "px"; var scrollHeight = Math.max(totalHeight, d.scroller.scrollHeight); - var needsH = d.scroller.scrollWidth > d.scroller.clientWidth; - var needsV = scrollHeight > d.scroller.clientHeight; + var needsH = d.scroller.scrollWidth > (d.scroller.clientWidth + 1); + var needsV = scrollHeight > (d.scroller.clientHeight + 1); if (needsV) { d.scrollbarV.style.display = "block"; d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0"; - d.scrollbarV.firstChild.style.height = + d.scrollbarV.firstChild.style.height = (scrollHeight - d.scroller.clientHeight + d.scrollbarV.clientHeight) + "px"; - } else d.scrollbarV.style.display = ""; + } else { + d.scrollbarV.style.display = ""; + d.scrollbarV.firstChild.style.height = "0"; + } 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 = ""; + } else { + d.scrollbarH.style.display = ""; + d.scrollbarH.firstChild.style.width = "0"; + } 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 (needsH && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = scrollbarWidth(d.measure) + "px"; + d.gutterFiller.style.width = d.gutters.offsetWidth + "px"; + } else d.gutterFiller.style.display = ""; - if (mac_geLion && scrollbarWidth(d.measure) === 0) + if (mac_geLion && scrollbarWidth(d.measure) === 0) { d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = mac_geMountainLion ? "18px" : "12px"; + d.scrollbarV.style.pointerEvents = d.scrollbarH.style.pointerEvents = "none"; + } } function visibleLines(display, doc, viewPort) { @@ -393,24 +410,43 @@ window.CodeMirror = (function() { // DISPLAY DRAWING - function updateDisplay(cm, changes, viewPort) { - var oldFrom = cm.display.showingFrom, oldTo = cm.display.showingTo; - var updated = updateDisplayInner(cm, changes, viewPort); + function updateDisplay(cm, changes, viewPort, forced) { + var oldFrom = cm.display.showingFrom, oldTo = cm.display.showingTo, updated; + var visible = visibleLines(cm.display, cm.doc, viewPort); + for (var first = true;; first = false) { + var oldWidth = cm.display.scroller.clientWidth; + if (!updateDisplayInner(cm, changes, visible, forced)) break; + updated = true; + changes = []; + updateSelection(cm); + updateScrollbars(cm); + if (first && cm.options.lineWrapping && oldWidth != cm.display.scroller.clientWidth) { + forced = true; + continue; + } + forced = false; + + // 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; + } + if (updated) { signalLater(cm, "update", cm); if (cm.display.showingFrom != oldFrom || cm.display.showingTo != oldTo) signalLater(cm, "viewportChange", cm, cm.display.showingFrom, cm.display.showingTo); } - updateSelection(cm); - updateScrollbars(cm.display, cm.doc.height); - 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, viewPort) { + function updateDisplayInner(cm, changes, visible, forced) { var display = cm.display, doc = cm.doc; if (!display.wrapper.clientWidth) { display.showingFrom = display.showingTo = doc.first; @@ -418,12 +454,8 @@ window.CodeMirror = (function() { 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(display, doc, viewPort); // Bail out if the visible area is already rendered and nothing changed. - if (changes.length == 0 && + if (!forced && changes.length == 0 && visible.from > display.showingFrom && visible.to < display.showingTo) return; @@ -436,7 +468,7 @@ window.CodeMirror = (function() { var positionsChangedFrom = Infinity; if (cm.options.lineNumbers) for (var i = 0; i < changes.length; ++i) - if (changes[i].diff) { positionsChangedFrom = changes[i].from; break; } + if (changes[i].diff && changes[i].from < positionsChangedFrom) { positionsChangedFrom = changes[i].from; } var end = doc.first + doc.size; var from = Math.max(visible.from - cm.options.viewportMargin, doc.first); @@ -476,26 +508,39 @@ window.CodeMirror = (function() { 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) { + if (!forced && intactLines == to - from && from == display.showingFrom && to == display.showingTo) { updateViewOffset(cm); return; } intact.sort(function(a, b) {return a.from - b.from;}); - var focused = document.activeElement; + // 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 (document.activeElement != focused && focused.offsetHeight) focused.focus(); + 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; + if (different) { + display.lastSizeC = display.wrapper.clientHeight; + startWorker(cm, 400); + } display.showingFrom = from; display.showingTo = to; - startWorker(cm, 100); + updateHeightsInViewport(cm); + updateViewOffset(cm); + + return true; + } + + function updateHeightsInViewport(cm) { + var display = cm.display; var prevBottom = display.lineDiv.offsetTop; for (var node = display.lineDiv.firstChild, height; node; node = node.nextSibling) if (node.lineObj) { if (ie_lt8) { @@ -515,11 +560,6 @@ window.CodeMirror = (function() { widgets[i].height = widgets[i].node.offsetHeight; } } - updateViewOffset(cm); - - if (visibleLines(display, doc, viewPort).to > to) - updateDisplayInner(cm, [], viewPort); - return true; } function updateViewOffset(cm) { @@ -585,8 +625,9 @@ window.CodeMirror = (function() { 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) { + if (line.widgets && cur && cur.previousSibling) for (var i = 0; i < line.widgets.length; ++i) { + var w = line.widgets[i]; + if (w.showIfHidden) { var prev = cur.previousSibling; if (/pre/i.test(prev.nodeName)) { var wrap = elt("div", null, null, "position: relative"); @@ -594,9 +635,11 @@ window.CodeMirror = (function() { wrap.appendChild(prev); prev = wrap; } - var wnode = prev.appendChild(elt("div", [line.widgets[i].node], "CodeMirror-linewidget")); - positionLineWidget(line.widgets[i], wnode, prev, dims); + var wnode = prev.appendChild(elt("div", [w.node], "CodeMirror-linewidget")); + if (!w.handleMouseEvents) wnode.ignoreEvents = true; + positionLineWidget(w, 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. @@ -627,37 +670,38 @@ window.CodeMirror = (function() { } function buildLineElement(cm, line, lineNo, dims, reuse) { - var lineElement = lineContent(cm, line); + var built = buildLineContent(cm, line), lineElement = built.pre; var markers = line.gutterMarkers, display = cm.display, wrap; - if (!cm.options.lineNumbers && !markers && !line.bgClass && !line.wrapClass && !line.widgets) + var bgClass = built.bgClass ? built.bgClass + " " + (line.bgClass || "") : line.bgClass; + if (!cm.options.lineNumbers && !markers && !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; + var isOk = true, widgetsSeen = 0, insertBefore = null; 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; } + for (var i = 0; i < line.widgets.length; ++i) { + var widget = line.widgets[i]; if (widget.node == n.firstChild) { + if (!widget.above && !insertBefore) insertBefore = n; positionLineWidget(widget, n, reuse, dims); ++widgetsSeen; - if (isFirst) reuse.insertBefore(lineElement, n); break; } } if (i == line.widgets.length) { isOk = false; break; } } } + reuse.insertBefore(lineElement, insertBefore); if (isOk && widgetsSeen == line.widgets.length) { wrap = reuse; reuse.className = line.wrapClass || ""; @@ -668,8 +712,8 @@ window.CodeMirror = (function() { wrap.appendChild(lineElement); } // Kludge to make sure the styled element lies behind the selection (by z-index) - if (line.bgClass) - wrap.insertBefore(elt("div", "\u00a0", line.bgClass + " CodeMirror-linebackground"), wrap.firstChild); + if (bgClass) + wrap.insertBefore(elt("div", null, 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"), @@ -692,6 +736,7 @@ window.CodeMirror = (function() { 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"); + if (!widget.handleMouseEvents) node.ignoreEvents = true; positionLineWidget(widget, node, wrap, dims); if (widget.above) wrap.insertBefore(node, cm.options.lineNumbers && line.height != 0 ? gutterWrap : lineElement); @@ -735,12 +780,14 @@ window.CodeMirror = (function() { display.selectionDiv.style.display = "none"; // Move the hidden textarea near the cursor to prevent scrolling artifacts - 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"; + 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 @@ -772,67 +819,59 @@ window.CodeMirror = (function() { "px; height: " + (bottom - top) + "px")); } - function drawForLine(line, fromArg, toArg, retTop) { + function drawForLine(line, fromArg, toArg) { 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); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias); } iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { - var leftPos = coords(dir == "rtl" ? to - 1 : from); - var rightPos = coords(dir == "rtl" ? from : to - 1); - var left = leftPos.left, right = rightPos.right; + var leftPos = coords(from, "left"), rightPos, left, right; + if (from == to) { + rightPos = leftPos; + left = right = leftPos.left; + } else { + rightPos = coords(to - 1, "right"); + if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } + left = leftPos.left; + right = rightPos.right; + } + if (fromArg == null && from == 0) left = pl; 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 (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) + start = leftPos; + if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) + end = rightPos; if (left < pl + 1) left = pl; add(left, rightPos.top, right - left, rightPos.bottom); }); - return rVal; + return {start: start, end: end}; } 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; + var fromLine = getLine(doc, sel.from.line), toLine = getLine(doc, sel.to.line); + var singleVLine = visualLine(doc, fromLine) == visualLine(doc, toLine); + var leftEnd = drawForLine(sel.from.line, sel.from.ch, singleVLine ? fromLine.text.length : null).end; + var rightStart = drawForLine(sel.to.line, singleVLine ? 0 : null, sel.to.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(pl, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); } - 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); } + if (leftEnd.bottom < rightStart.top) + add(pl, leftEnd.bottom, null, rightStart.top); } removeChildrenAndAdd(display.selectionDiv, fragment); @@ -841,14 +880,15 @@ window.CodeMirror = (function() { // 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() { - if (!display.cursor.offsetHeight) return; - display.cursor.style.visibility = display.otherCursor.style.visibility = (on = !on) ? "" : "hidden"; - }, cm.options.cursorBlinkRate); + if (cm.options.cursorBlinkRate > 0) + display.blinker = setInterval(function() { + display.cursor.style.visibility = display.otherCursor.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); } // HIGHLIGHT WORKER @@ -898,12 +938,13 @@ window.CodeMirror = (function() { // 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) { + function findStartLine(cm, n, precise) { var minindent, minline, doc = cm.doc; - for (var search = n, lim = n - 100; search > lim; --search) { + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { if (search <= doc.first) return doc.first; var line = getLine(doc, search - 1); - if (line.stateAfter) return search; + if (line.stateAfter && (!precise || search <= doc.frontier)) return search; var indented = countColumn(line.text, null, cm.options.tabSize); if (minline == null || minindent > indented) { minline = search - 1; @@ -913,10 +954,10 @@ window.CodeMirror = (function() { return minline; } - function getStateBefore(cm, n) { + function getStateBefore(cm, n, precise) { 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 (!doc.mode.startState) return true; + var pos = findStartLine(cm, n, precise), 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) { @@ -925,55 +966,79 @@ window.CodeMirror = (function() { line.stateAfter = save ? copyState(doc.mode, state) : null; ++pos; }); + if (precise) doc.frontier = 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) { + function measureChar(cm, line, ch, data, bias) { var dir = -1; data = data || measureLine(cm, line); - + if (data.crude) { + var left = data.left + ch * data.width; + return {left: left, right: left + data.width, top: data.top, bottom: data.bottom}; + } + for (var pos = ch;; pos += dir) { var r = data[pos]; if (r) break; if (dir < 0 && pos == 0) dir = 1; } + bias = pos > ch ? "left" : pos < ch ? "right" : bias; + if (bias == "left" && r.leftSide) r = r.leftSide; + else if (bias == "right" && r.rightSide) r = r.rightSide; return {left: pos < ch ? r.right : r.left, right: pos > ch ? r.left : r.right, - top: r.top, bottom: r.bottom}; + 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.wrapClass) + return memo; + } + } + + function clearCachedMeasurement(cm, line) { + var exists = findCachedMeasurement(cm, line); + if (exists) exists.text = exists.measure = exists.markedSpans = null; } function measureLine(cm, line) { // First look in the cache - var display = cm.display, 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 && - display.scroller.clientWidth == memo.width && - memo.classes == line.textClass + "|" + line.bgClass + "|" + line.wrapClass) - return memo.measure; - } - + var cached = findCachedMeasurement(cm, line); + if (cached) return cached.measure; + + // Failing that, recompute and store result in cache var measure = measureLineInner(cm, line); - // Store result in the cache - var memo = {text: line.text, width: display.scroller.clientWidth, + 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[++display.measureLineCachePos % 16] = memo; + classes: line.textClass + "|" + line.wrapClass}; + if (cache.length == 16) cache[++cm.display.measureLineCachePos % 16] = memo; else cache.push(memo); return measure; } function measureLineInner(cm, line) { + if (!cm.options.lineWrapping && line.text.length >= cm.options.crudeMeasuringFrom) + return crudelyMeasureLine(cm, line); + var display = cm.display, measure = emptyArray(line.text.length); - var pre = lineContent(cm, line, measure); + var pre = buildLineContent(cm, line, measure, true).pre; // IE does not cache element positions of inline elements between // calls to getBoundingClientRect. This makes the loop below, @@ -1009,45 +1074,90 @@ window.CodeMirror = (function() { 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]; + function measureRect(rect) { + var top = rect.top - outer.top, bot = rect.bottom - outer.top; + if (bot > maxBot) bot = maxBot; + if (top < 0) top = 0; + for (var i = vranges.length - 2; i >= 0; i -= 2) { + var rtop = vranges[i], rbot = vranges[i+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); + vranges[i] = Math.min(top, rtop); + vranges[i+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}; + if (i < 0) { i = vranges.length; vranges.push(top, bot); } + return {left: rect.left - outer.left, + right: rect.right - outer.left, + top: i, bottom: null}; } - 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]; - } - if (!cm.options.lineWrapping) { - var last = pre.lastChild; - if (last.nodeType == 3) last = pre.appendChild(elt("span", "\u200b")); - data.width = getRect(last).right - outer.left; + function finishRect(rect) { + rect.bottom = vranges[rect.top+1]; + rect.top = vranges[rect.top]; } + for (var i = 0, cur; i < measure.length; ++i) if (cur = measure[i]) { + var node = cur, rect = null; + // A widget might wrap, needs special care + if (/\bCodeMirror-widget\b/.test(cur.className) && cur.getClientRects) { + if (cur.firstChild.nodeType == 1) node = cur.firstChild; + var rects = node.getClientRects(); + if (rects.length > 1) { + rect = data[i] = measureRect(rects[0]); + rect.rightSide = measureRect(rects[rects.length - 1]); + } + } + if (!rect) rect = data[i] = measureRect(getRect(node)); + if (cur.measureRight) rect.right = getRect(cur.measureRight).left; + if (cur.leftSide) rect.leftSide = measureRect(getRect(cur.leftSide)); + } + removeChildren(cm.display.measure); + for (var i = 0, cur; i < data.length; ++i) if (cur = data[i]) { + finishRect(cur); + if (cur.leftSide) finishRect(cur.leftSide); + if (cur.rightSide) finishRect(cur.rightSide); + } return data; } + function crudelyMeasureLine(cm, line) { + var copy = new Line(line.text.slice(0, 100), null); + if (line.textClass) copy.textClass = line.textClass; + var measure = measureLineInner(cm, copy); + var left = measureChar(cm, copy, 0, measure, "left"); + var right = measureChar(cm, copy, 99, measure, "right"); + return {crude: true, top: left.top, left: left.left, bottom: left.bottom, width: (right.right - left.left) / 100}; + } + + 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 || line.text.length >= cm.options.crudeMeasuringFrom) + return measureChar(cm, line, line.text.length, cached && cached.measure, "right").right; + + var pre = buildLineContent(cm, line, null, true).pre; + 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; - cm.display.maxLineChanged = true; + if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; cm.display.lineNumChars = null; } + function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } + function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } + // 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) { @@ -1057,60 +1167,75 @@ window.CodeMirror = (function() { if (context == "line") return rect; if (!context) context = "local"; var yOff = heightAtLine(cm, lineObj); - if (context != "local") yOff -= cm.display.viewOffset; - if (context == "page") { + if (context == "local") yOff += paddingTop(cm.display); + else yOff -= cm.display.viewOffset; + if (context == "page" || context == "window") { 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); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); rect.left += xOff; rect.right += xOff; } rect.top += yOff; rect.bottom += yOff; return rect; } - function charCoords(cm, pos, context, lineObj) { + // 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; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = getRect(cm.display.sizer); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = getRect(cm.display.lineSpace); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; + } + + function charCoords(cm, pos, context, lineObj, bias) { if (!lineObj) lineObj = getLine(cm.doc, pos.line); - return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch), context); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, null, bias), 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); + var m = measureChar(cm, lineObj, ch, measurement, right ? "right" : "left"); if (right) m.left = m.right; else m.right = m.left; return intoCoordSystem(cm, lineObj, m, context); } + function getBidi(ch, partPos) { + var part = order[partPos], right = part.level % 2; + if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { + part = order[--partPos]; + ch = bidiRight(part) - (part.level % 2 ? 0 : 1); + right = true; + } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { + part = order[++partPos]; + ch = bidiLeft(part) - part.level % 2; + right = false; + } + if (right && ch == part.to && ch > part.from) return get(ch - 1); + return get(ch, right); + } 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; + var partPos = getBidiPartAt(order, ch); + var val = getBidi(ch, partPos); + if (bidiOther != null) val.other = getBidi(ch, bidiOther); + return val; } - function PosMaybeOutside(line, ch, outside) { + function PosWithInfo(line, ch, outside, xRel) { var pos = new Pos(line, ch); + pos.xRel = xRel; if (outside) pos.outside = true; return pos; } @@ -1119,10 +1244,10 @@ window.CodeMirror = (function() { function coordsChar(cm, x, y) { var doc = cm.doc; y += cm.display.viewOffset; - if (y < 0) return PosMaybeOutside(doc.first, 0, true); + if (y < 0) return PosWithInfo(doc.first, 0, true, -1); 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); + return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); if (x < 0) x = 0; for (;;) { @@ -1130,7 +1255,7 @@ window.CodeMirror = (function() { var found = coordsCharInner(cm, lineObj, lineNo, x, y); var merged = collapsedSpanAtEnd(lineObj); var mergedPos = merged && merged.find(); - if (merged && found.ch >= mergedPos.from.ch) + if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) lineNo = mergedPos.to.line; else return found; @@ -1139,15 +1264,15 @@ window.CodeMirror = (function() { function coordsCharInner(cm, lineObj, lineNo, x, y) { var innerOff = y - heightAtLine(cm, lineObj); - var wrongLine = false, cWidth = cm.display.wrapper.clientWidth; + 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 Math.max(0, sp.left - cWidth); - else if (innerOff < sp.top) return sp.left + cWidth; + if (innerOff > sp.bottom) return sp.left - adjust; + else if (innerOff < sp.top) return sp.left + adjust; else wrongLine = false; return sp.left; } @@ -1156,14 +1281,15 @@ window.CodeMirror = (function() { 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); + if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); // 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; + var ch = x < fromX || x - fromX <= toX - x ? from : to; + var xDiff = x - (ch == from ? fromX : toX); while (isExtendingChar.test(lineObj.text.charAt(ch))) ++ch; - var pos = PosMaybeOutside(lineNo, ch, after ? fromOutside : toOutside); - pos.after = after; + var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, + xDiff < 0 ? -1 : xDiff ? 1 : 0); return pos; } var step = Math.ceil(dist / 2), middle = from + step; @@ -1172,8 +1298,8 @@ window.CodeMirror = (function() { 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;} + if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} + else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} } } @@ -1220,10 +1346,12 @@ window.CodeMirror = (function() { // An array of ranges of lines that have to be updated. See // updateDisplay. changes: [], + forceUpdate: false, updateInput: null, userSelChange: null, textChanged: null, selectionChanged: false, + cursorActivity: false, updateMaxLine: false, updateScrollPos: false, id: ++nextOpId @@ -1236,8 +1364,8 @@ window.CodeMirror = (function() { cm.curOp = null; if (op.updateMaxLine) computeMaxLength(cm); - if (display.maxLineChanged && !cm.options.lineWrapping) { - var width = measureLine(cm, display.maxLine).width; + 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); @@ -1251,13 +1379,18 @@ window.CodeMirror = (function() { 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 (op.changes.length || op.forceUpdate || newScrollPos && newScrollPos.scrollTop != null) { + updated = updateDisplay(cm, op.changes, newScrollPos && newScrollPos.scrollTop, op.forceUpdate); + 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.from), + clipPos(cm.doc, op.scrollToPos.to), op.scrollToPos.margin); } else if (newScrollPos) { scrollCursorIntoView(cm); } @@ -1279,7 +1412,7 @@ window.CodeMirror = (function() { } if (op.textChanged) signal(cm, "change", cm, op.textChanged); - if (op.selectionChanged) signal(cm, "cursorActivity", cm); + if (op.cursorActivity) signal(cm, "cursorActivity", cm); if (delayed) for (var i = 0; i < delayed.length; ++i) delayed[i](); } @@ -1344,32 +1477,37 @@ window.CodeMirror = (function() { // 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; + if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.state.disableInput) return false; + if (cm.state.pasteIncoming && cm.state.fakedLastChar) { + input.value = input.value.substring(0, input.value.length - 1); + cm.state.fakedLastChar = false; + } var text = input.value; if (text == prevInput && posEq(sel.from, sel.to)) return false; - // IE enjoys randomly deselecting our input's text when - // re-focusing. If the selection is gone but the cursor is at the - // start of the input, that's probably what happened. - if (ie && text && input.selectionStart === 0) { + 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[same] == text[same]) ++same; + 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"); - + var changeEvent = {from: from, to: to, text: splitLines(text.slice(same)), + origin: cm.state.pasteIncoming ? "paste" : "+input"}; + makeChange(cm.doc, changeEvent, "end"); cm.curOp.updateInput = updateInput; - if (text.length > 1000) input.value = cm.display.prevInput = ""; + signalLater(cm, "inputRead", cm, changeEvent); + + 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; @@ -1382,10 +1520,14 @@ window.CodeMirror = (function() { cm.display.prevInput = ""; minimal = hasCopyEvent && (doc.sel.to.line - doc.sel.from.line > 100 || (selected = cm.getSelection()).length > 1000); - if (minimal) cm.display.input.value = "-"; - else cm.display.input.value = selected || cm.getSelection(); + var content = minimal ? "-" : selected || cm.getSelection(); + cm.display.input.value = content; if (cm.state.focused) selectInput(cm.display.input); - } else if (user) cm.display.prevInput = cm.display.input.value = ""; + 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; } @@ -1403,7 +1545,17 @@ window.CodeMirror = (function() { function registerEventHandlers(cm) { var d = cm.display; on(d.scroller, "mousedown", operation(cm, onMouseDown)); - on(d.scroller, "dblclick", operation(cm, e_preventDefault)); + if (ie) + on(d.scroller, "dblclick", operation(cm, function(e) { + if (signalDOMEvent(cm, e)) return; + 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", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); on(d.lineSpace, "selectstart", function(e) { if (!eventInWidget(d, e)) e_preventDefault(e); }); @@ -1413,15 +1565,17 @@ window.CodeMirror = (function() { if (!captureMiddleClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); on(d.scroller, "scroll", function() { - setScrollTop(cm, d.scroller.scrollTop); - setScrollLeft(cm, d.scroller.scrollLeft, true); - signal(cm, "scroll", cm); + if (d.scroller.clientHeight) { + setScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } }); on(d.scrollbarV, "scroll", function() { - setScrollTop(cm, d.scrollbarV.scrollTop); + if (d.scroller.clientHeight) setScrollTop(cm, d.scrollbarV.scrollTop); }); on(d.scrollbarH, "scroll", function() { - setScrollLeft(cm, d.scrollbarH.scrollLeft); + if (d.scroller.clientHeight) setScrollLeft(cm, d.scrollbarH.scrollLeft); }); on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); @@ -1433,11 +1587,15 @@ window.CodeMirror = (function() { // Prevent wrapper from ever scrolling on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + var resizeTimer; function onResize() { - // Might be a text scaling operation, clear size caches. - d.cachedCharWidth = d.cachedTextHeight = null; - clearCaches(cm); - runInOp(cm, bind(regChange, cm)); + if (resizeTimer == null) resizeTimer = setTimeout(function() { + resizeTimer = null; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = knownScrollbarWidth = null; + clearCaches(cm); + runInOp(cm, bind(regChange, cm)); + }, 100); } on(window, "resize", onResize); // Above handler holds on to the editor and its data structures. @@ -1447,21 +1605,24 @@ window.CodeMirror = (function() { 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 (signalDOMEvent(cm, e) || 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, "input", function() { + if (ie && !ie_lt9 && cm.display.inputHasSelection) cm.display.inputHasSelection = null; + 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 (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))) return; + if (signalDOMEvent(cm, e) || cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))) return; e_stop(e); } if (cm.options.dragDrop) { @@ -1470,12 +1631,22 @@ window.CodeMirror = (function() { on(d.scroller, "dragover", drag_); on(d.scroller, "drop", operation(cm, onDrop)); } - on(d.scroller, "paste", function(e){ + on(d.scroller, "paste", function(e) { if (eventInWidget(d, e)) return; - focusInput(cm); + focusInput(cm); fastPoll(cm); }); on(d.input, "paste", function() { + // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 + // Add a char to the end of textarea before paste occur so that + // selection doesn't span to the end of textarea. + if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) { + var start = d.input.selectionStart, end = d.input.selectionEnd; + d.input.value += "$"; + d.input.selectionStart = start; + d.input.selectionEnd = end; + cm.state.fakedLastChar = true; + } cm.state.pasteIncoming = true; fastPoll(cm); }); @@ -1500,9 +1671,7 @@ window.CodeMirror = (function() { 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; + if (!n || n.ignoreEvents || n.parentNode == display.sizer && n != display.mover) return true; } } @@ -1512,7 +1681,7 @@ window.CodeMirror = (function() { 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; + target == display.scrollbarFiller || target == display.gutterFiller) return null; } var x, y, space = getRect(display.lineSpace); // Fails unpredictably on IE[67] when mouse is dragged around quickly. @@ -1522,6 +1691,7 @@ window.CodeMirror = (function() { var lastClick, lastDoubleClick; function onMouseDown(e) { + if (signalDOMEvent(this, e)) return; var cm = this, display = cm.display, doc = cm.doc, sel = doc.sel; sel.shift = e.shiftKey; @@ -1540,6 +1710,7 @@ window.CodeMirror = (function() { if (captureMiddleClick) onContextMenu.call(cm, cm, e); return; case 2: + if (webkit) cm.state.lastMiddleDown = +new Date; if (start) extendSelection(cm.doc, start); setTimeout(bind(focusInput, cm), 20); e_preventDefault(e); @@ -1592,9 +1763,12 @@ window.CodeMirror = (function() { e_preventDefault(e); if (type == "single") extendSelection(cm.doc, clipPos(doc, start)); - var startstart = sel.from, startend = sel.to; + 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; @@ -1642,8 +1816,6 @@ window.CodeMirror = (function() { function done(e) { counter = Infinity; - var cur = posFromMouse(cm, e); - if (cur) doSelect(cur); e_preventDefault(e); focusInput(cm); off(document, "mousemove", move); @@ -1659,11 +1831,48 @@ window.CodeMirror = (function() { on(document, "mouseup", up); } + function gutterEvent(cm, e, type, prevent, signalfn) { + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + if (mX >= Math.floor(getRect(cm.display.gutters).right)) return false; + if (prevent) e_preventDefault(e); + + var display = cm.display; + var lineBox = getRect(display.lineDiv); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); + 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]; + signalfn(cm, type, cm, line, gutter, e); + return e_defaultPrevented(e); + } + } + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false, signal); + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true, signalLater); + } + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + function onDrop(e) { var cm = this; - if (eventInWidget(cm.display, e) || (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e)))) + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e) || (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e)))) return; e_preventDefault(e); + if (ie) lastDrop = +new Date; var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; if (!pos || isReadOnly(cm)) return; if (files && files.length && window.FileReader && window.File) { @@ -1674,7 +1883,7 @@ window.CodeMirror = (function() { text[i] = reader.result; if (++read == n) { pos = clipPos(cm.doc, pos); - replaceRange(cm.doc, text.join(""), pos, "around", "paste"); + makeChange(cm.doc, {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}, "around"); } }; reader.readAsText(file); @@ -1703,34 +1912,10 @@ window.CodeMirror = (function() { } } - 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 (eventInWidget(cm.display, e)) return; - + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; + var txt = cm.getSelection(); e.dataTransfer.setData("Text", txt); @@ -1738,6 +1923,7 @@ window.CodeMirror = (function() { // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. if (e.dataTransfer.setDragImage && !safari) { var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; if (opera) { img.width = img.height = 1; cm.display.wrapper.appendChild(img); @@ -1756,6 +1942,7 @@ window.CodeMirror = (function() { 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, []); + startWorker(cm, 100); } function setScrollLeft(cm, val, isScroller) { if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; @@ -1793,6 +1980,11 @@ window.CodeMirror = (function() { 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 @@ -1806,7 +1998,6 @@ window.CodeMirror = (function() { } } - var display = cm.display, scroll = display.scroller; // 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 @@ -1873,8 +2064,8 @@ window.CodeMirror = (function() { function allKeyMaps(cm) { var maps = cm.state.keyMaps.slice(0); + if (cm.options.extraKeys) maps.push(cm.options.extraKeys); maps.push(cm.options.keyMap); - if (cm.options.extraKeys) maps.unshift(cm.options.extraKeys); return maps; } @@ -1884,8 +2075,10 @@ window.CodeMirror = (function() { var startMap = getKeyMap(cm.options.keyMap), next = startMap.auto; clearTimeout(maybeTransition); if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() { - if (getKeyMap(cm.options.keyMap) == startMap) + if (getKeyMap(cm.options.keyMap) == startMap) { cm.options.keyMap = (next.call ? next.call(null, cm) : next); + keyMapChanged(cm); + } }, 50); var name = keyName(e, true), handled = false; @@ -1898,17 +2091,18 @@ window.CodeMirror = (function() { // '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); + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + 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; } + signalLater(cm, "keyHandled", cm, name, e); } return handled; } @@ -1919,6 +2113,7 @@ window.CodeMirror = (function() { if (handled) { e_preventDefault(e); restartBlink(cm); + signalLater(cm, "keyHandled", cm, "'" + ch + "'", e); } return handled; } @@ -1927,8 +2122,8 @@ window.CodeMirror = (function() { 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; + if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + if (ie && e.keyCode == 27) e.returnValue = false; var code = e.keyCode; // IE does strange things with escape. cm.doc.sel.shift = code == 16 || e.shiftKey; @@ -1944,7 +2139,7 @@ window.CodeMirror = (function() { function onKeyPress(e) { var cm = this; - if (cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + if (signalDOMEvent(cm, e) || 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; @@ -1954,6 +2149,7 @@ window.CodeMirror = (function() { 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); } @@ -1964,7 +2160,10 @@ window.CodeMirror = (function() { cm.state.focused = true; if (cm.display.wrapper.className.search(/\bCodeMirror-focused\b/) == -1) cm.display.wrapper.className += " CodeMirror-focused"; - resetInput(cm, true); + if (!cm.curOp) { + resetInput(cm, true); + if (webkit) setTimeout(bind(resetInput, cm, true), 0); // Issue #1730 + } } slowPoll(cm); restartBlink(cm); @@ -1981,12 +2180,17 @@ window.CodeMirror = (function() { var detectingSelectAll; function onContextMenu(cm, e) { + if (signalDOMEvent(cm, e, "contextmenu")) return; var display = cm.display, sel = cm.doc.sel; - if (eventInWidget(display, e)) return; + if (eventInWidget(display, e) || contextMenuInGutter(cm, 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)) + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && (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; @@ -1999,19 +2203,24 @@ window.CodeMirror = (function() { // Adds "Select all" to context menu in FF if (posEq(sel.from, sel.to)) display.input.value = display.prevInput = " "; + function prepareSelectAllHack() { + if (display.input.selectionStart != null) { + var extval = display.input.value = "\u200b" + (posEq(sel.from, sel.to) ? "" : display.input.value); + display.prevInput = "\u200b"; + display.input.selectionStart = 1; display.input.selectionEnd = extval.length; + } + } 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)) { + // Try to detect the user choosing select-all + if (display.input.selectionStart != null) { + if (!ie || ie_lt9) prepareSelectAllHack(); 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(){ + var i = 0, poll = function(){ if (display.prevInput == " " && display.input.selectionStart == 0) operation(cm, commands.selectAll)(cm); else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500); @@ -2021,6 +2230,7 @@ window.CodeMirror = (function() { } } + if (ie && !ie_lt9) prepareSelectAllHack(); if (captureMiddleClick) { e_stop(e); var mouseup = function() { @@ -2035,10 +2245,11 @@ window.CodeMirror = (function() { // UPDATING - function changeEnd(change) { + var changeEnd = CodeMirror.changeEnd = function(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) { @@ -2063,7 +2274,7 @@ window.CodeMirror = (function() { 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}; @@ -2080,21 +2291,21 @@ window.CodeMirror = (function() { return {anchor: adjustPos(doc.sel.anchor), head: adjustPos(doc.sel.head)}; } - function filterChange(doc, change) { + function filterChange(doc, change, update) { 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; } }; + if (update) obj.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; + }; signal(doc, "beforeChange", doc, obj); if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); @@ -2111,7 +2322,7 @@ window.CodeMirror = (function() { } if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { - change = filterChange(doc, change); + change = filterChange(doc, change, true); if (!change) return; } @@ -2129,6 +2340,7 @@ window.CodeMirror = (function() { } function makeChangeNoReadonly(doc, change, selUpdate) { + if (change.text.length == 1 && change.text[0] == "" && posEq(change.from, change.to)) return; var selAfter = computeSelAfterChange(doc, change, selUpdate); addToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); @@ -2145,18 +2357,28 @@ window.CodeMirror = (function() { } 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}; + anchorAfter: event.anchorBefore, headAfter: event.headBefore, + generation: hist.generation}; (type == "undo" ? hist.undone : hist.done).push(anti); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); for (var i = event.changes.length - 1; i >= 0; --i) { var change = event.changes[i]; change.origin = type; + if (filter && !filterChange(doc, change, false)) { + (type == "undo" ? hist.done : hist.undone).length = 0; + return; + } + anti.changes.push(historyChangeFromChange(doc, change)); var after = i ? computeSelAfterChange(doc, change, null) @@ -2205,6 +2427,8 @@ window.CodeMirror = (function() { 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); @@ -2224,6 +2448,9 @@ window.CodeMirror = (function() { }); } + 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) { @@ -2246,8 +2473,12 @@ window.CodeMirror = (function() { 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, origin: change.origin}; + 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; @@ -2332,6 +2563,7 @@ window.CodeMirror = (function() { var sel = doc.sel; sel.goalColumn = null; + if (bias == null) bias = posLess(head, sel.head) ? -1 : 1; // Skip over atomic spans. if (checkAtomic || !posEq(anchor, sel.anchor)) anchor = skipAtomic(doc, anchor, bias, checkAtomic != "push"); @@ -2346,7 +2578,8 @@ window.CodeMirror = (function() { sel.to = inv ? anchor : head; if (doc.cm) - doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true; + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = + doc.cm.curOp.cursorActivity = true; signalLater(doc, "cursorActivity", doc); } @@ -2360,16 +2593,20 @@ window.CodeMirror = (function() { var dir = bias || 1; doc.cantEdit = false; search: for (;;) { - var line = getLine(doc, curPos.line), toClear; + 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 && m.clearOnEnter) { - (toClear || (toClear = [])).push(m); - continue; - } else if (!m.atomic) continue; + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } + } + if (!m.atomic) continue; var newPos = m.find()[dir < 0 ? "from" : "to"]; if (posEq(newPos, curPos)) { newPos.ch += dir; @@ -2396,7 +2633,6 @@ window.CodeMirror = (function() { continue search; } } - if (toClear) for (var i = 0; i < toClear.length; ++i) toClear[i].clear(); } return curPos; } @@ -2405,7 +2641,7 @@ window.CodeMirror = (function() { // SCROLLING function scrollCursorIntoView(cm) { - var coords = scrollPosIntoView(cm, cm.doc.sel.head); + var coords = scrollPosIntoView(cm, cm.doc.sel.head, null, cm.options.cursorScrollMargin); if (!cm.state.focused) return; var display = cm.display, box = getRect(display.sizer), doScroll = null; if (coords.top + box.top < 0) doScroll = true; @@ -2422,10 +2658,15 @@ window.CodeMirror = (function() { } } - function scrollPosIntoView(cm, pos) { + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) margin = 0; for (;;) { var changed = false, coords = cursorCoords(cm, pos); - var scrollPos = calculateScrollPos(cm, coords.left, coords.top, coords.left, coords.bottom); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), + Math.min(coords.top, endCoords.top) - margin, + Math.max(coords.left, endCoords.left), + Math.max(coords.bottom, endCoords.bottom) + margin); var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; if (scrollPos.scrollTop != null) { setScrollTop(cm, scrollPos.scrollTop); @@ -2446,13 +2687,17 @@ window.CodeMirror = (function() { } function calculateScrollPos(cm, x1, y1, x2, y2) { - var display = cm.display, pt = paddingTop(display); - y1 += pt; y2 += pt; + var display = cm.display, snapMargin = textHeight(cm.display); + if (y1 < 0) y1 = 0; var screen = display.scroller.clientHeight - scrollerCutOff, screentop = display.scroller.scrollTop, result = {}; - var docBottom = cm.doc.height + 2 * pt; - 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 docBottom = cm.doc.height + paddingVert(display); + var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; + 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; @@ -2467,11 +2712,23 @@ window.CodeMirror = (function() { 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 == null) how = "add"; if (how == "smart") { if (!cm.doc.mode.indent) how = "prev"; else var state = getStateBefore(cm, n); @@ -2494,6 +2751,8 @@ window.CodeMirror = (function() { indentation = curSpace + cm.options.indentUnit; } else if (how == "subtract") { indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; } indentation = Math.max(0, indentation); @@ -2502,7 +2761,9 @@ window.CodeMirror = (function() { for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} if (pos < indentation) indentString += spaceStr(indentation - pos); - if (indentString != curSpaceString) + if (indentString == curSpaceString) + setSelection(cm.doc, Pos(n, curSpaceString.length), Pos(n, curSpaceString.length)); + else replaceRange(cm.doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); line.stateAfter = null; } @@ -2518,7 +2779,7 @@ window.CodeMirror = (function() { } function findPosH(doc, pos, dir, unit, visually) { - var line = pos.line, ch = pos.ch; + var line = pos.line, ch = pos.ch, origDir = dir; var lineObj = getLine(doc, line); var possible = true; function findNextLine() { @@ -2540,16 +2801,24 @@ window.CodeMirror = (function() { 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; + 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; + } + if (type) sawType = type; + if (dir > 0 && !moveOnce(!first)) break; } } - var result = skipAtomic(doc, Pos(line, ch), dir, true); + var result = skipAtomic(doc, Pos(line, ch), origDir, true); if (!possible) result.hitSide = true; return result; } @@ -2558,7 +2827,7 @@ window.CodeMirror = (function() { 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; + 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; } @@ -2574,11 +2843,11 @@ window.CodeMirror = (function() { function findWordAt(line, pos) { var start = pos.ch, end = pos.ch; if (line) { - if (pos.after === false || end == line.length) --start; else ++end; + if ((pos.xRel < 0 || end == line.length) && start) --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);}; + 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; } @@ -2595,6 +2864,7 @@ window.CodeMirror = (function() { // 'wrap f in an operation, performed on its `this` parameter' CodeMirror.prototype = { + constructor: CodeMirror, focus: function(){window.focus(); focusInput(this); onFocus(this); fastPoll(this);}, setOption: function(option, value) { @@ -2608,13 +2878,13 @@ window.CodeMirror = (function() { getOption: function(option) {return this.options[option];}, getDoc: function() {return this.doc;}, - addKeyMap: function(map) { - this.state.keyMaps.push(map); + 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) { + if (maps[i] == map || (typeof maps[i] != "string" && maps[i].name == map)) { maps.splice(i, 1); return true; } @@ -2630,7 +2900,8 @@ window.CodeMirror = (function() { removeOverlay: operation(null, function(spec) { var overlays = this.state.overlays; for (var i = 0; i < overlays.length; ++i) { - if (overlays[i].modeSpec == spec) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { overlays.splice(i, 1); this.state.modeGen++; regChange(this); @@ -2640,7 +2911,7 @@ window.CodeMirror = (function() { }), indentLine: operation(null, function(n, dir, aggressive) { - if (typeof dir != "string") { + if (typeof dir != "string" && typeof dir != "number") { if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; else dir = dir ? "add" : "subtract"; } @@ -2655,10 +2926,10 @@ window.CodeMirror = (function() { // Fetch the parser token for a given character. Useful for hacks // that want to inspect the mode state (say, for completion). - getTokenAt: function(pos) { + getTokenAt: function(pos, precise) { var doc = this.doc; pos = clipPos(doc, pos); - var state = getStateBefore(this, pos.line), mode = this.doc.mode; + var state = getStateBefore(this, pos.line, precise), 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()) { @@ -2673,10 +2944,37 @@ window.CodeMirror = (function() { state: state}; }, - getStateAfter: function(line) { + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + if (ch == 0) return styles[2]; + for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; + else if (styles[mid * 2 + 1] < ch) before = mid + 1; + else return styles[mid * 2 + 2]; + } + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) return mode; + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; + }, + + getHelper: function(pos, type) { + if (!helpers.hasOwnProperty(type)) return; + var help = helpers[type], mode = this.getModeAt(pos); + return mode[type] && help[mode[type]] || + mode.helperType && help[mode.helperType] || + help[mode.name]; + }, + + getStateAfter: function(line, precise) { var doc = this.doc; line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); - return getStateBefore(this, line + 1); + return getStateBefore(this, line + 1, precise); }, cursorCoords: function(start, mode) { @@ -2691,14 +2989,26 @@ window.CodeMirror = (function() { return charCoords(this, clipPos(this.doc, pos), mode || "page"); }, - coordsChar: function(coords) { - var off = getRect(this.display.lineSpace); - var scrollY = window.pageYOffset || (document.documentElement || document.body).scrollTop; - var scrollX = window.pageXOffset || (document.documentElement || document.body).scrollLeft; - return coordsChar(this, coords.left - off.left - scrollX, coords.top - off.top - scrollY); + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top); + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset); + }, + heightAtLine: function(line, mode) { + var end = false, last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) line = this.doc.first; + else if (line > last) { line = last; end = true; } + var lineObj = getLine(this.doc, line); + return intoCoordSystem(this, getLine(this.doc, line), {top: 0, left: 0}, mode || "page").top + + (end ? lineObj.height : 0); }, 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) { @@ -2725,7 +3035,7 @@ window.CodeMirror = (function() { 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 if (new RegExp("(?:^|\\s)" + cls + "(?:$|\\s)").test(line[prop])) return false; else line[prop] += " " + cls; return true; }); @@ -2738,9 +3048,10 @@ window.CodeMirror = (function() { 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; + var found = cur.match(new RegExp("(?:^|\\s+)" + cls + "(?:$|\\s+)")); + if (!found) return false; + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; } return true; }); @@ -2788,7 +3099,7 @@ window.CodeMirror = (function() { if (left + node.offsetWidth > hspace) left = hspace - node.offsetWidth; } - node.style.top = (top + paddingTop(display)) + "px"; + node.style.top = top + "px"; node.style.left = node.style.right = ""; if (horiz == "right") { left = display.sizer.clientWidth - node.offsetWidth; @@ -2851,21 +3162,22 @@ window.CodeMirror = (function() { if (sel.goalColumn != null) pos.left = sel.goalColumn; var target = findPosV(this, pos, dir, unit); - if (unit == "page") - this.display.scrollbarV.scrollTop += charCoords(this, target, "div").top - pos.top; + 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() { + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) return; 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) { - this.curOp.updateScrollPos = {scrollLeft: x, scrollTop: y}; + updateScrollPos(this, x, y); }), getScrollInfo: function() { var scroller = this.display.scroller, co = scrollerCutOff; @@ -2874,34 +3186,45 @@ window.CodeMirror = (function() { clientHeight: scroller.clientHeight - co, clientWidth: scroller.clientWidth - co}; }, - scrollIntoView: function(pos) { - if (typeof pos == "number") pos = Pos(pos, 0); - if (!pos || pos.line != null) { - pos = pos ? clipPos(this.doc, pos) : this.doc.sel.head; - scrollPosIntoView(this, pos); - } else { - scrollIntoView(this, pos.left, pos.top, pos.right, pos.bottom); - } - }, + scrollIntoView: operation(null, function(range, margin) { + if (range == null) range = {from: this.doc.sel.head, to: null}; + else if (typeof range == "number") range = {from: Pos(range, 0), to: null}; + else if (range.from == null) range = {from: range, to: null}; + if (!range.to) range.to = range.from; + if (!margin) margin = 0; - setSize: function(width, height) { + var coords = range; + if (range.from.line != null) { + this.curOp.scrollToPos = {from: range.from, to: range.to, margin: margin}; + coords = {from: cursorCoords(this, range.from), + to: cursorCoords(this, range.to)}; + } + var sPos = calculateScrollPos(this, Math.min(coords.from.left, coords.to.left), + Math.min(coords.from.top, coords.to.top) - margin, + Math.max(coords.from.right, coords.to.right), + Math.max(coords.from.bottom, coords.to.bottom) + margin); + updateScrollPos(this, sPos.scrollLeft, sPos.scrollTop); + }), + + setSize: operation(null, function(width, height) { function interpret(val) { return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; } 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);}, + if (this.options.lineWrapping) + this.display.measureLineCache.length = this.display.measureLineCachePos = 0; + this.curOp.forceUpdate = true; + }), operation: function(f){return runInOp(this, f);}, refresh: operation(null, function() { + var badHeight = this.display.cachedTextHeight == null; clearCaches(this); - this.curOp.updateScrollPos = {scrollTop: this.doc.scrollTop, scrollLeft: this.doc.scrollLeft}; + updateScrollPos(this, this.doc.scrollLeft, this.doc.scrollTop); regChange(this); + if (badHeight) estimateLineHeights(this); }), swapDoc: operation(null, function(doc) { @@ -2909,7 +3232,9 @@ window.CodeMirror = (function() { old.cm = null; attachDoc(this, doc); clearCaches(this); - this.curOp.updateScrollPos = {scrollTop: doc.scrollTop, scrollLeft: doc.scrollLeft}; + resetInput(this, true); + updateScrollPos(this, doc.scrollLeft, doc.scrollTop); + signalLater(this, "swapDoc", this, old); return old; }), @@ -2918,6 +3243,7 @@ window.CodeMirror = (function() { getScrollerElement: function(){return this.display.scroller;}, getGutterElement: function(){return this.display.gutters;} }; + eventMixin(CodeMirror); // OPTION DEFAULTS @@ -2974,6 +3300,7 @@ window.CodeMirror = (function() { cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; cm.refresh(); }, true); + option("coverGutterNextToScrollbar", false, updateScrollbars, true); option("lineNumbers", false, function(cm) { setGuttersForLineNumbers(cm.options); guttersChanged(cm); @@ -2981,7 +3308,9 @@ window.CodeMirror = (function() { option("firstLineNumber", 1, guttersChanged, true); option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); option("showCursorWhenSelecting", false, updateSelection, true); - + + option("resetSelectionOnContextMenu", true); + option("readOnly", false, function(cm, val) { if (val == "nocursor") {onBlur(cm); cm.display.input.blur();} else if (!val) resetInput(cm, true); @@ -2989,13 +3318,20 @@ window.CodeMirror = (function() { option("dragDrop", true); option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); 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("crudeMeasuringFrom", 10000); + 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 || ""; @@ -3021,16 +3357,21 @@ window.CodeMirror = (function() { }; CodeMirror.resolveMode = function(spec) { - if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { spec = mimeModes[spec]; - else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { return CodeMirror.resolveMode("application/xml"); + } if (typeof spec == "string") return {name: spec}; else return spec || {name: "null"}; }; CodeMirror.getMode = function(options, spec) { - spec = CodeMirror.resolveMode(spec); + var spec = CodeMirror.resolveMode(spec); var mfactory = modes[spec.name]; if (!mfactory) return CodeMirror.getMode(options, "text/plain"); var modeObj = mfactory(options, spec); @@ -3043,6 +3384,7 @@ window.CodeMirror = (function() { } } modeObj.name = spec.name; + return modeObj; }; @@ -3062,12 +3404,24 @@ window.CodeMirror = (function() { CodeMirror.defineExtension = function(name, func) { CodeMirror.prototype[name] = func; }; - + CodeMirror.defineDocExtension = function(name, func) { + Doc.prototype[name] = func; + }; CodeMirror.defineOption = option; var initHooks = []; CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + var helpers = CodeMirror.helpers = {}; + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {}; + helpers[type][name] = value; + }; + + // UTILITIES + + CodeMirror.isWordChar = isWordChar; + // MODE STATE HANDLING // Utility functions for working with state. Exported because modes @@ -3093,6 +3447,7 @@ window.CodeMirror = (function() { CodeMirror.innerMode = function(mode, state) { while (mode.innerMode) { var info = mode.innerMode(state); + if (!info || info.mode == mode) break; state = info.state; mode = info.mode; } @@ -3113,6 +3468,10 @@ window.CodeMirror = (function() { var l = cm.getCursor().line; cm.replaceRange("", Pos(l, 0), Pos(l), "+delete"); }, + delLineLeft: function(cm) { + var cur = cm.getCursor(); + cm.replaceRange("", Pos(cur.line, 0), cur, "+delete"); + }, undo: function(cm) {cm.undo();}, redo: function(cm) {cm.redo();}, goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, @@ -3133,6 +3492,14 @@ window.CodeMirror = (function() { 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");}, @@ -3142,11 +3509,15 @@ window.CodeMirror = (function() { 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");}, @@ -3176,7 +3547,8 @@ window.CodeMirror = (function() { 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", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", "Enter": "newlineAndIndent", "Insert": "toggleOverwrite" }; // Note that the save and find-related commands aren't defined by @@ -3184,19 +3556,19 @@ window.CodeMirror = (function() { 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": "delWordBefore", "Ctrl-Delete": "delWordAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "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": "goWordLeft", - "Alt-Right": "goWordRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delWordBefore", - "Ctrl-Alt-Backspace": "delWordAfter", "Alt-Delete": "delWordAfter", "Cmd-S": "save", "Cmd-F": "find", + "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", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delLineLeft", fallthrough: ["basic", "emacsy"] }; keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; @@ -3235,7 +3607,7 @@ window.CodeMirror = (function() { for (var i = 0; i < maps.length; ++i) { var done = lookup(maps[i]); - if (done) return done; + if (done) return done != "stop"; } } function isModifierKey(event) { @@ -3243,6 +3615,7 @@ window.CodeMirror = (function() { 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; @@ -3262,6 +3635,8 @@ window.CodeMirror = (function() { 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) { @@ -3274,17 +3649,19 @@ window.CodeMirror = (function() { function save() {textarea.value = cm.getValue();} if (textarea.form) { - // Deplorable hack to make the submit method do the right thing. on(textarea.form, "submit", save); - var form = textarea.form, realSubmit = form.submit; - try { - var wrappedSubmit = form.submit = function() { - save(); - form.submit = realSubmit; - form.submit(); - form.submit = wrappedSubmit; - }; - } catch(e) {} + // 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"; @@ -3316,6 +3693,7 @@ window.CodeMirror = (function() { this.pos = this.start = 0; this.string = string; this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; } StringStream.prototype = { @@ -3348,12 +3726,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; } @@ -3376,11 +3761,16 @@ window.CodeMirror = (function() { this.doc = doc; } CodeMirror.TextMarker = TextMarker; + eventMixin(TextMarker); TextMarker.prototype.clear = function() { if (this.explicitlyCleared) return; var cm = this.doc.cm, withOp = cm && !cm.curOp; if (withOp) startOperation(cm); + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) signalLater(this, "clear", found.from, found.to); + } var min = null, max = null; for (var i = 0; i < this.lines.length; ++i) { var line = this.lines[i]; @@ -3404,12 +3794,11 @@ window.CodeMirror = (function() { if (min != null && cm) regChange(cm, min, max + 1); this.lines.length = 0; this.explicitlyCleared = true; - if (this.collapsed && this.doc.cantEdit) { + if (this.atomic && this.doc.cantEdit) { this.doc.cantEdit = false; if (cm) reCheckSelection(cm); } if (withOp) endOperation(cm); - signalLater(this, "clear"); }; TextMarker.prototype.find = function() { @@ -3427,16 +3816,21 @@ window.CodeMirror = (function() { 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, - clearOnEnter: this.clearOnEnter, - replacedWith: copyWidget ? repl && repl.cloneNode(true) : repl, - readOnly: this.readOnly, - startStyle: this.startStyle, endStyle: this.endStyle}; + TextMarker.prototype.changed = function() { + var pos = this.find(), cm = this.doc.cm; + if (!pos || !cm) return; + if (this.type != "bookmark") pos = pos.from; + var line = getLine(this.doc, pos.line); + clearCachedMeasurement(cm, line); + if (pos.line >= cm.display.showingFrom && pos.line < cm.display.showingTo) { + for (var node = cm.display.lineDiv.firstChild; node; node = node.nextSibling) if (node.lineObj == line) { + if (node.offsetHeight != line.height) updateLineHeight(line, node.offsetHeight); + break; + } + runInOp(cm, function() { + cm.curOp.selectionChanged = cm.curOp.forceUpdate = cm.curOp.updateMaxLine = true; + }); + } }; TextMarker.prototype.attachLine = function(line) { @@ -3460,14 +3854,21 @@ window.CodeMirror = (function() { 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 (posLess(to, from) || posEq(from, to) && type == "range" && + !(options.inclusiveLeft && options.inclusiveRight)) + return marker; if (options) copyObj(options, marker); if (marker.replacedWith) { marker.collapsed = true; marker.replacedWith = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.replacedWith.ignoreEvents = true; } 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) @@ -3488,6 +3889,8 @@ window.CodeMirror = (function() { 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) @@ -3501,7 +3904,7 @@ window.CodeMirror = (function() { } if (cm) { if (updateMaxLine) cm.curOp.updateMaxLine = true; - if (marker.className || marker.startStyle || marker.endStyle || marker.collapsed) + if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.collapsed) regChange(cm, from.line, to.line + 1); if (marker.atomic) reCheckSelection(cm); } @@ -3519,6 +3922,7 @@ window.CodeMirror = (function() { } } CodeMirror.SharedTextMarker = SharedTextMarker; + eventMixin(SharedTextMarker); SharedTextMarker.prototype.clear = function() { if (this.explicitlyCleared) return; @@ -3530,17 +3934,14 @@ window.CodeMirror = (function() { 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; @@ -3571,7 +3972,9 @@ window.CodeMirror = (function() { 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 && (!isInsert || !span.marker.insertLeft)) { + if (startsBefore || + (marker.inclusiveLeft && marker.inclusiveRight || 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, @@ -3635,6 +4038,13 @@ window.CodeMirror = (function() { } } } + if (sameLine && first) { + // Make sure we didn't create any zero-length spans + for (var i = 0; i < first.length; ++i) + if (first[i].from != null && first[i].from == first[i].to && first[i].marker.type != "bookmark") + first.splice(i--, 1); + if (!first.length) first = null; + } var newMarkers = [first]; if (!sameLine) { @@ -3729,6 +4139,7 @@ window.CodeMirror = (function() { sp = sps[i]; if (!sp.marker.collapsed) continue; if (sp.from == null) return true; + if (sp.marker.replacedWith) continue; if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) return true; } @@ -3742,7 +4153,7 @@ window.CodeMirror = (function() { return true; for (var sp, i = 0; i < line.markedSpans.length; ++i) { sp = line.markedSpans[i]; - if (sp.marker.collapsed && sp.from == span.to && + if (sp.marker.collapsed && !sp.marker.replacedWith && sp.from == span.to && (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && lineIsHiddenInner(doc, line, sp)) return true; } @@ -3766,11 +4177,12 @@ window.CodeMirror = (function() { // LINE WIDGETS var LineWidget = CodeMirror.LineWidget = function(cm, node, options) { - for (var opt in options) if (options.hasOwnProperty(opt)) + if (options) for (var opt in options) if (options.hasOwnProperty(opt)) this[opt] = options[opt]; this.cm = cm; this.node = node; }; + eventMixin(LineWidget); function widgetOperation(f) { return function() { var withOp = !this.cm.curOp; @@ -3785,7 +4197,9 @@ window.CodeMirror = (function() { 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; + var aboveVisible = heightAtLine(this.cm, this.line) < this.cm.doc.scrollTop; updateLineHeight(this.line, Math.max(0, this.line.height - widgetHeight(this))); + if (aboveVisible) addToScrollPos(this.cm, 0, -this.height); regChange(this.cm, no, no + 1); }); LineWidget.prototype.changed = widgetOperation(function() { @@ -3809,14 +4223,14 @@ window.CodeMirror = (function() { 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); + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) widgets.push(widget); + else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); widget.line = line; if (!lineIsHidden(cm.doc, line) || widget.showIfHidden) { - var aboveVisible = heightAtLine(cm, line) < cm.display.scroller.scrollTop; + var aboveVisible = heightAtLine(cm, line) < cm.doc.scrollTop; updateLineHeight(line, line.height + widgetHeight(widget)); - if (aboveVisible) - cm.curOp.updateScrollPos = {scrollTop: cm.doc.scrollTop + widget.height, - scrollLeft: cm.doc.scrollLeft}; + if (aboveVisible) addToScrollPos(cm, 0, widget.height); } return true; }); @@ -3827,12 +4241,12 @@ window.CodeMirror = (function() { // Line objects. These hold state related to a line, including // highlighting info (the styles array). - function makeLine(text, markedSpans, estimateHeight) { - var line = {text: text}; - attachMarkedSpans(line, markedSpans); - line.height = estimateHeight ? estimateHeight(line) : 1; - return line; - } + var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + eventMixin(Line); function updateLine(line, text, markedSpans, estimateHeight) { line.text = text; @@ -3843,7 +4257,6 @@ window.CodeMirror = (function() { attachMarkedSpans(line, markedSpans); var estHeight = estimateHeight ? estimateHeight(line) : 1; if (estHeight != line.height) updateLineHeight(line, estHeight); - signalLater(line, "change"); } function cleanUpLine(line) { @@ -3855,26 +4268,31 @@ window.CodeMirror = (function() { // array, which contains alternating fragments of text and CSS // classes. function runMode(cm, text, mode, state, f) { - var flattenSpans = cm.options.flattenSpans; - var curText = "", curStyle = null; - var stream = new StringStream(text, cm.options.tabSize); + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize), style; if (text == "" && mode.blankLine) mode.blankLine(state); while (!stream.eol()) { - var style = mode.token(stream, state); - if (stream.pos > 5000) { + 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); + stream.pos = text.length; style = null; + } else { + style = mode.token(stream, state); } - 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 (curStart < stream.start) f(stream.start, curStyle); + curStart = stream.start; curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 characters + var pos = Math.min(stream.pos, curStart + 50000); + f(pos, curStyle); + curStart = pos; } - if (curText) f(curText, curStyle); } function highlightLine(cm, line, state) { @@ -3882,27 +4300,24 @@ window.CodeMirror = (function() { // 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);}); + runMode(cm, line.text, cm.doc.mode, state, function(end, style) {st.push(end, 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; + var overlay = cm.state.overlays[o], i = 1, at = 0; + runMode(cm, line.text, overlay.mode, true, function(end, style) { + var start = i; // 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; - } + while (at < end) { + var i_end = st[i]; + if (i_end > end) + st.splice(i, 1, end, st[i+1], i_end); i += 2; + at = Math.min(end, i_end); } if (!style) return; if (overlay.opaque) { - st.splice(start, i - start, txt, style); + st.splice(start, i - start, end, style); i = start + 2; } else { for (; start < i; start += 2) { @@ -3928,49 +4343,55 @@ window.CodeMirror = (function() { 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 <= 5000) { + while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { mode.token(stream, state); stream.start = stream.pos; } } var styleToClassCache = {}; - function styleToClass(style) { + function interpretTokenStyle(style, builder) { if (!style) return null; + for (;;) { + var lineClass = style.match(/(?:^|\s)line-(background-)?(\S+)/); + if (!lineClass) break; + style = style.slice(0, lineClass.index) + style.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (builder[prop] == null) + builder[prop] = lineClass[2]; + else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(builder[prop])) + builder[prop] += " " + lineClass[2]; + } 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; + function buildLineContent(cm, realLine, measure, copyWidgets) { + var merged, line = realLine, empty = true; + while (merged = collapsedSpanAtStart(line)) 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; + var builder = {pre: elt("pre"), col: 0, pos: 0, + measure: null, measuredSomething: false, cm: cm, + copyWidgets: copyWidgets}; do { + if (line.text) empty = false; builder.measure = line == realLine && measure; builder.pos = 0; builder.addToken = builder.measure ? buildTokenMeasure : buildToken; - if (measure && sawBefore && line != realLine && !builder.addedOne) { - measure[0] = builder.pre.appendChild(zeroWidthElement(cm.display.measure)); - builder.addedOne = true; - } + if ((ie || webkit) && cm.getOption("lineWrapping")) + builder.addToken = buildTokenSplitSpaces(builder.addToken); var next = insertLineContent(line, builder, getLineStyles(cm, line)); - sawBefore = line == lineBefore; - if (next) { - line = getLine(cm.doc, next.to.line); - simple = false; + if (measure && line == realLine && !builder.measuredSomething) { + measure[0] = builder.pre.appendChild(zeroWidthElement(cm.display.measure)); + builder.measuredSomething = true; } + if (next) line = getLine(cm.doc, next.to.line); } while (next); - if (measure && !builder.addedOne) - measure[0] = builder.pre.appendChild(simple ? elt("span", "\u00a0") : zeroWidthElement(cm.display.measure)); + if (measure && !builder.measuredSomething && !measure[0]) + measure[0] = builder.pre.appendChild(empty ? elt("span", "\u00a0") : zeroWidthElement(cm.display.measure)); if (!builder.pre.firstChild && !lineIsHidden(cm.doc, realLine)) builder.pre.appendChild(document.createTextNode("\u00a0")); @@ -3989,11 +4410,15 @@ window.CodeMirror = (function() { } } - return builder.pre; + var textClass = builder.textClass ? builder.textClass + " " + (realLine.textClass || "") : realLine.textClass; + if (textClass) builder.pre.className = textClass; + + signal(cm, "renderLine", cm, realLine, builder.pre); + return builder; } - var tokenSpecialChars = /[\t\u0000-\u0019\u200b\u2028\u2029\uFEFF]/g; - function buildToken(builder, text, style, startStyle, endStyle) { + var tokenSpecialChars = /[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\uFEFF]/g; + function buildToken(builder, text, style, startStyle, endStyle, title) { if (!text) return; if (!tokenSpecialChars.test(text)) { builder.col += text.length; @@ -4026,36 +4451,69 @@ window.CodeMirror = (function() { var fullStyle = style || ""; if (startStyle) fullStyle += startStyle; if (endStyle) fullStyle += endStyle; - return builder.pre.appendChild(elt("span", [content], fullStyle)); + var token = elt("span", [content], fullStyle); + if (title) token.title = title; + return builder.pre.appendChild(token); } 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 && builder.cm.options.lineWrapping && - spanAffectsWrapping.test(text.slice(i - 1, i + 1))) { + } else if (i && wrapping && spanAffectsWrapping(text, i)) { builder.pre.appendChild(elt("wbr")); } - builder.measure[builder.pos] = + var old = builder.measure[builder.pos]; + var span = builder.measure[builder.pos] = buildToken(builder, ch, style, start && startStyle, i == text.length - 1 && endStyle); + if (old) span.leftSide = old.leftSide || old; + // 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; + if (text.length) builder.measuredSomething = true; } - function buildCollapsedSpan(builder, size, widget) { + 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, title) { + return inner(builder, text.replace(/ {3,}/g, split), style, startStyle, endStyle, title); + }; + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.replacedWith; if (widget) { - if (!builder.display) widget = widget.cloneNode(true); + if (builder.copyWidgets) widget = widget.cloneNode(true); builder.pre.appendChild(widget); - if (builder.measure && size) { - builder.measure[builder.pos] = widget; - builder.addedOne = true; + if (builder.measure) { + if (size) { + builder.measure[builder.pos] = widget; + } else { + var elt = zeroWidthElement(builder.cm.display.measure); + if (marker.type == "bookmark" && !marker.insertLeft) + builder.measure[builder.pos] = builder.pre.appendChild(elt); + else if (builder.measure[builder.pos]) + return; + else + builder.measure[builder.pos] = builder.pre.insertBefore(elt, widget); + } + builder.measuredSomething = true; } } builder.pos += size; @@ -4064,21 +4522,20 @@ window.CodeMirror = (function() { // 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; + var spans = line.markedSpans, allText = line.text, at = 0; if (!spans) { for (var i = 1; i < styles.length; i+=2) - builder.addToken(builder, styles[i], styleToClass(styles[i+1])); + builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder)); return; } - var allText = line.text, len = allText.length; - var pos = 0, i = 1, text = "", style; - var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, collapsed; + var len = allText.length, pos = 0, i = 1, text = "", style; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; for (;;) { if (nextChange == pos) { // Update current marker set - spanStyle = spanEndStyle = spanStartStyle = ""; + spanStyle = spanEndStyle = spanStartStyle = title = ""; collapsed = null; nextChange = Infinity; - var foundBookmark = null; + var foundBookmarks = []; 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)) { @@ -4086,20 +4543,21 @@ window.CodeMirror = (function() { 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)) + if (m.title && !title) title = m.title; + if (m.collapsed && (!collapsed || collapsed.marker.size < m.size)) 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 (m.type == "bookmark" && sp.from == pos && m.replacedWith) foundBookmarks.push(m); } if (collapsed && (collapsed.from || 0) == pos) { buildCollapsedSpan(builder, (collapsed.to == null ? len : collapsed.to) - pos, - collapsed.from != null && collapsed.marker.replacedWith); + collapsed.marker, collapsed.from == null); if (collapsed.to == null) return collapsed.marker.find(); } - if (foundBookmark && !collapsed) buildCollapsedSpan(builder, 0, foundBookmark); + if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) + buildCollapsedSpan(builder, 0, foundBookmarks[j]); } if (pos >= len) break; @@ -4110,13 +4568,14 @@ window.CodeMirror = (function() { 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 : ""); + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title); } if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} pos = end; spanStartStyle = ""; } - text = styles[i++]; style = styleToClass(styles[i++]); + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder); } } } @@ -4125,6 +4584,10 @@ window.CodeMirror = (function() { 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); @@ -4135,30 +4598,28 @@ window.CodeMirror = (function() { // 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)); - updateLine(lastLine, lastLine.text, lastSpans, estimateHeight); + added.push(new Line(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) { - updateLine(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), - lastSpans, estimateHeight); + 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)); - updateLine(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0), estimateHeight); + added.push(new Line(text[i], spansFor(i), estimateHeight)); + added.push(new Line(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) { - updateLine(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), - spansFor(0), estimateHeight); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); doc.remove(from.line + 1, nlines); } else { - updateLine(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0), estimateHeight); - updateLine(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans, estimateHeight); + 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)); + added.push(new Line(text[i], spansFor(i), estimateHeight)); if (nlines > 1) doc.remove(from.line + 1, nlines - 1); doc.insert(from.line + 1, added); } @@ -4300,12 +4761,13 @@ window.CodeMirror = (function() { 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)])]); + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); this.first = firstLine; this.scrollTop = this.scrollLeft = 0; this.cantEdit = false; this.history = makeHistory(); + this.cleanGeneration = 1; this.frontier = firstLine; var start = Pos(firstLine, 0); this.sel = {from: start, to: start, head: start, anchor: start, shift: false, extend: false, goalColumn: null}; @@ -4317,6 +4779,7 @@ window.CodeMirror = (function() { }; Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, 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); @@ -4357,13 +4820,18 @@ window.CodeMirror = (function() { replaceRange(this, text, Pos(line, 0), clipPos(this, Pos(line))); }, removeLine: function(line) { - if (isLine(this, line)) - replaceRange(this, "", Pos(line, 0), clipPos(this, Pos(line + 1, 0))); + 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);}, + getLineHandleVisualStart: function(line) { + if (typeof line == "number") line = getLine(this, line); + return visualLine(this, line); + }, + lineCount: function() {return this.size;}, firstLine: function() {return this.first;}, lastLine: function() {return this.first + this.size - 1;}, @@ -4385,11 +4853,11 @@ window.CodeMirror = (function() { if (extend) extendSelection(this, pos); else setSelection(this, pos, pos); }), - setSelection: docOperation(function(anchor, head) { - setSelection(this, clipPos(this, anchor), clipPos(this, head || anchor)); + setSelection: docOperation(function(anchor, head, bias) { + setSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), bias); }), - extendSelection: docOperation(function(from, to) { - extendSelection(this, clipPos(this, from), to && clipPos(this, to)); + extendSelection: docOperation(function(from, to, bias) { + extendSelection(this, clipPos(this, from), to && clipPos(this, to), bias); }), getSelection: function(lineSep) {return this.getRange(this.sel.from, this.sel.to, lineSep);}, @@ -4405,20 +4873,25 @@ window.CodeMirror = (function() { var hist = this.history; return {undo: hist.done.length, redo: hist.undone.length}; }, - clearHistory: function() {this.history = makeHistory();}, + clearHistory: function() {this.history = makeHistory(this.history.maxGeneration);}, markClean: function() { - this.history.dirtyCounter = 0; - this.history.lastOp = this.history.lastOrigin = null; + this.cleanGeneration = this.changeGeneration(); }, - isClean: function () {return this.history.dirtyCounter == 0;}, - + changeGeneration: function() { + this.history.lastOp = this.history.lastOrigin = null; + return this.history.generation; + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration); + }, + getHistory: function() { return {done: copyHistoryArray(this.history.done), undone: copyHistoryArray(this.history.undone)}; }, setHistory: function(histData) { - var hist = this.history = makeHistory(); + var hist = this.history = makeHistory(this.history.maxGeneration); hist.done = histData.done.slice(0); hist.undone = histData.undone.slice(0); }, @@ -4529,6 +5002,8 @@ window.CodeMirror = (function() { return function() {return method.apply(this.doc, arguments);}; })(Doc.prototype[prop]); + eventMixin(Doc); + function linkedDocs(doc, f, sharedHistOnly) { function propagate(doc, skip, sharedHist) { if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { @@ -4648,7 +5123,7 @@ window.CodeMirror = (function() { // HISTORY - function makeHistory() { + function makeHistory(startGen) { return { // Arrays of history events. Doing something adds an event to // done and clears undo. Undoing moves events from done to @@ -4658,7 +5133,7 @@ window.CodeMirror = (function() { // event lastTime: 0, lastOp: null, lastOrigin: null, // Used by the isClean() method - dirtyCounter: 0 + generation: startGen || 1, maxGeneration: startGen || 1 }; } @@ -4672,7 +5147,8 @@ window.CodeMirror = (function() { } function historyChangeFromChange(doc, change) { - var histChange = {from: change.from, to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + var from = { line: change.from.line, ch: change.from.ch }; + var histChange = {from: 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; @@ -4686,7 +5162,8 @@ window.CodeMirror = (function() { if (cur && (hist.lastOp == opId || hist.lastOrigin == change.origin && change.origin && - ((change.origin.charAt(0) == "+" && hist.lastTime > time - 600) || change.origin.charAt(0) == "*"))) { + ((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)) { @@ -4701,17 +5178,13 @@ window.CodeMirror = (function() { } else { // Can not be merged, start a new event. cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation, anchorBefore: doc.sel.anchor, headBefore: doc.sel.head, anchorAfter: selAfter.anchor, headAfter: selAfter.head}; hist.done.push(cur); + hist.generation = ++hist.maxGeneration; 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; @@ -4826,6 +5299,9 @@ window.CodeMirror = (function() { if (e.stopPropagation) e.stopPropagation(); else e.cancelBubble = true; } + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; + } function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} CodeMirror.e_stop = e_stop; CodeMirror.e_preventDefault = e_preventDefault; @@ -4892,6 +5368,11 @@ window.CodeMirror = (function() { delayedCallbacks.push(bnd(arr[i])); } + function signalDOMEvent(cm, e, override) { + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore; + } + function fireDelayed() { --delayedCallbackDepth; var delayed = delayedCallbacks; @@ -4906,6 +5387,11 @@ window.CodeMirror = (function() { CodeMirror.on = on; CodeMirror.off = off; CodeMirror.signal = signal; + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + // MISC UTILITIES // Number of pixels added to scroller and sizer to hide scrollbar @@ -4920,12 +5406,12 @@ window.CodeMirror = (function() { // 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; } @@ -4946,7 +5432,11 @@ window.CodeMirror = (function() { if (ios) { // Mobile Safari apparently has a bug where select() is broken. node.selectionStart = 0; node.selectionEnd = node.value.length; - } else node.select(); + } else { + // Suppress mysterious IE10 errors + try { node.select(); } + catch(_e) {} + } } function indexOf(collection, elt) { @@ -4980,7 +5470,7 @@ window.CodeMirror = (function() { return function(){return f.apply(null, args);}; } - var nonASCIISingleCaseWordChar = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc]/; + var nonASCIISingleCaseWordChar = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; function isWordChar(ch) { return /\w/.test(ch) || ch > "\x80" && (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); @@ -5005,9 +5495,8 @@ window.CodeMirror = (function() { } function removeChildren(e) { - // IE will break all parent-child relations in subnodes when setting innerHTML - if (!ie) e.innerHTML = ""; - else while (e.firstChild) e.removeChild(e.firstChild); + for (var count = e.childNodes.length; count > 0; --count) + e.removeChild(e.firstChild); return e; } @@ -5042,13 +5531,31 @@ window.CodeMirror = (function() { // 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 + // extra span. This terribly fragile set of tests 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 = /\-[^ \-\.?]|\?[^ \-\.?\]\}:;!'\"\),\/]|[\.!\"#&%\)*+,:;=>\]|\}~][\(\{\[<]|\$'/; + function spanAffectsWrapping() { return false; } + if (gecko) // Only for "$'" + spanAffectsWrapping = function(str, i) { + return str.charCodeAt(i - 1) == 36 && str.charCodeAt(i) == 39; + }; + else if (safari && !/Version\/([6-9]|\d\d)\b/.test(navigator.userAgent)) + spanAffectsWrapping = function(str, i) { + return /\-[^ \-?]|\?[^ !\'\"\),.\-\/:;\?\]\}]/.test(str.slice(i - 1, i + 1)); + }; + else if (webkit && /Chrome\/(?:29|[3-9]\d|\d\d\d)\./.test(navigator.userAgent)) + spanAffectsWrapping = function(str, i) { + var code = str.charCodeAt(i - 1); + return code >= 8208 && code <= 8212; + }; + else if (webkit) + spanAffectsWrapping = function(str, i) { + if (i > 1 && str.charCodeAt(i - 1) == 45) { + if (/\w/.test(str.charAt(i - 2)) && /[^\-?\.]/.test(str.charAt(i))) return true; + if (i > 2 && /[\d\.,]/.test(str.charAt(i - 2)) && /[\d\.,]/.test(str.charAt(i))) return false; + } + return /[~!#%&*)=+}\]\\|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|…[\w~`@#$%\^&*(_=+{[><]/.test(str.slice(i - 1, i + 1)); + }; var knownScrollbarWidth; function scrollbarWidth(measure) { @@ -5133,11 +5640,15 @@ window.CodeMirror = (function() { function iterateBidiSections(order, from, to, f) { if (!order) return f(from, to, "ltr"); + var found = false; for (var i = 0; i < order.length; ++i) { var part = order[i]; - if (part.from < to && part.to > from || from == to && part.to == from) + 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"); + found = true; + } } + if (!found) f(from, to, "ltr"); } function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } @@ -5167,6 +5678,40 @@ window.CodeMirror = (function() { return Pos(lineN, ch); } + function compareBidiLevel(order, a, b) { + var linedir = order[0].level; + if (a == linedir) return true; + if (b == linedir) return false; + return a < b; + } + var bidiOther; + function getBidiPartAt(order, pos) { + for (var i = 0, found; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < pos && cur.to > pos) { bidiOther = null; return i; } + if (cur.from == pos || cur.to == pos) { + if (found == null) { + found = i; + } else if (compareBidiLevel(order, cur.level, order[found].level)) { + bidiOther = found; + return i; + } else { + bidiOther = i; + return found; + } + } + } + bidiOther = null; + return found; + } + + function moveInLine(line, pos, dir, byUnit) { + if (!byUnit) return pos + dir; + do pos += dir; + while (pos > 0 && isExtendingChar.test(line.text.charAt(pos))); + return pos; + } + // 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 @@ -5176,37 +5721,24 @@ window.CodeMirror = (function() { 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); + var pos = getBidiPartAt(bidi, start), part = bidi[pos]; + var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); - 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; + for (;;) { + if (target > part.from && target < part.to) return target; + if (target == part.from || target == part.to) { + if (getBidiPartAt(bidi, target) == pos) return target; + part = bidi[pos += dir]; + return (dir > 0) == part.level % 2 ? part.to : part.from; } 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; + part = bidi[pos += dir]; + if (!part) return null; + if ((dir > 0) == part.level % 2) + target = moveInLine(line, part.to, -1, byUnit); + else + target = moveInLine(line, part.from, 1, byUnit); } } - - return target < 0 || target > line.text.length ? null : target; } function moveLogically(line, start, dir, byUnit) { @@ -5378,7 +5910,7 @@ window.CodeMirror = (function() { // THE END - CodeMirror.version = "3.1"; + CodeMirror.version = "3.19.1"; return CodeMirror; })(); diff --git a/plugins/tiddlywiki/codemirror/styles.tid b/plugins/tiddlywiki/codemirror/styles.tid index 406492539..ec9d41458 100644 --- a/plugins/tiddlywiki/codemirror/styles.tid +++ b/plugins/tiddlywiki/codemirror/styles.tid @@ -5,6 +5,7 @@ tags: [[$:/tags/stylesheet]] .CodeMirror { height: auto; + border: 1px solid #ddd; } .CodeMirror-scroll {