From c528791671487baac0604d342a0eeab3fd5a49e9 Mon Sep 17 00:00:00 2001 From: "Brian J. Cardiff" Date: Thu, 7 Jun 2018 17:44:45 +0200 Subject: [PATCH] Update to codemirror 5.38.0 --- .../addon/comment/comment.js | 23 +- .../lib/codemirror.css | 29 +- .../lib/codemirror.js | 3095 ++++++++++------- .../mode/crystal/crystal.js | 88 +- .../theme/neat.css | 0 .../tools/playground/views/layout.html.ecr | 10 +- 6 files changed, 1934 insertions(+), 1311 deletions(-) rename src/compiler/crystal/tools/playground/public/vendor/{codemirror-5.22.0 => codemirror-5.38.0}/addon/comment/comment.js (93%) rename src/compiler/crystal/tools/playground/public/vendor/{codemirror-5.22.0 => codemirror-5.38.0}/lib/codemirror.css (91%) rename src/compiler/crystal/tools/playground/public/vendor/{codemirror-5.22.0 => codemirror-5.38.0}/lib/codemirror.js (81%) rename src/compiler/crystal/tools/playground/public/vendor/{codemirror-5.22.0 => codemirror-5.38.0}/mode/crystal/crystal.js (82%) rename src/compiler/crystal/tools/playground/public/vendor/{codemirror-5.22.0 => codemirror-5.38.0}/theme/neat.css (100%) diff --git a/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/addon/comment/comment.js b/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/addon/comment/comment.js similarity index 93% rename from src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/addon/comment/comment.js rename to src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/addon/comment/comment.js index d71cf4360377..84c67edf7898 100644 --- a/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/addon/comment/comment.js +++ b/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/addon/comment/comment.js @@ -46,12 +46,17 @@ // Rough heuristic to try and detect lines that are part of multi-line string function probablyInsideString(cm, pos, line) { - return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"`]/.test(line) + return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line) + } + + function getMode(cm, pos) { + var mode = cm.getMode() + return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos) } CodeMirror.defineExtension("lineComment", function(from, to, options) { if (!options) options = noOptions; - var self = this, mode = self.getModeAt(from); + var self = this, mode = getMode(self, from); var firstLine = self.getLine(from.line); if (firstLine == null || probablyInsideString(self, from, firstLine)) return; @@ -95,7 +100,7 @@ CodeMirror.defineExtension("blockComment", function(from, to, options) { if (!options) options = noOptions; - var self = this, mode = self.getModeAt(from); + var self = this, mode = getMode(self, from); var startString = options.blockCommentStart || mode.blockCommentStart; var endString = options.blockCommentEnd || mode.blockCommentEnd; if (!startString || !endString) { @@ -129,7 +134,7 @@ CodeMirror.defineExtension("uncomment", function(from, to, options) { if (!options) options = noOptions; - var self = this, mode = self.getModeAt(from); + var self = this, mode = getMode(self, from); var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end); // Try finding line comments @@ -167,13 +172,11 @@ if (open == -1) return false var endLine = end == start ? startLine : self.getLine(end) var close = endLine.indexOf(endString, end == start ? open + startString.length : 0); - if (close == -1 && start != end) { - endLine = self.getLine(--end); - close = endLine.indexOf(endString); - } + var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1) if (close == -1 || - !/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) || - !/comment/.test(self.getTokenTypeAt(Pos(end, close + 1)))) + !/comment/.test(self.getTokenTypeAt(insideStart)) || + !/comment/.test(self.getTokenTypeAt(insideEnd)) || + self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1) return false; // Avoid killing block comments completely outside the selection. diff --git a/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/lib/codemirror.css b/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/lib/codemirror.css similarity index 91% rename from src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/lib/codemirror.css rename to src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/lib/codemirror.css index 2a6a262282ba..c7a8ae70478f 100644 --- a/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/lib/codemirror.css +++ b/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/lib/codemirror.css @@ -5,6 +5,7 @@ font-family: monospace; height: 300px; color: black; + direction: ltr; } /* PADDING */ @@ -58,7 +59,12 @@ .cm-fat-cursor div.CodeMirror-cursors { z-index: 1; } - +.cm-fat-cursor-mark { + background-color: rgba(20, 255, 20, 0.5); + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} .cm-animate-fat-cursor { width: auto; border: 0; @@ -119,7 +125,7 @@ .cm-s-default .cm-property, .cm-s-default .cm-operator {} .cm-s-default .cm-variable-2 {color: #05a;} -.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} .cm-s-default .cm-comment {color: #a50;} .cm-s-default .cm-string {color: #a11;} .cm-s-default .cm-string-2 {color: #f50;} @@ -139,8 +145,8 @@ /* Default styles for common addons */ -div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} -div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } .CodeMirror-activeline-background {background: #e8f2ff;} @@ -223,11 +229,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} cursor: default; z-index: 4; } -.CodeMirror-gutter-wrapper { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} +.CodeMirror-gutter-wrapper ::selection { background-color: transparent } +.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } .CodeMirror-lines { cursor: text; @@ -267,11 +270,13 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} .CodeMirror-linewidget { position: relative; z-index: 2; - overflow: auto; + padding: 0.1px; /* Force widget margins to stay inside of the container */ } .CodeMirror-widget {} +.CodeMirror-rtl pre { direction: rtl; } + .CodeMirror-code { outline: none; } @@ -320,8 +325,8 @@ div.CodeMirror-dragcursors { .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } .cm-searching { - background: #ffa; - background: rgba(255, 255, 0, .4); + background-color: #ffa; + background-color: rgba(255, 255, 0, .4); } /* Used to force a border model for a node */ diff --git a/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/lib/codemirror.js b/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/lib/codemirror.js similarity index 81% rename from src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/lib/codemirror.js rename to src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/lib/codemirror.js index 460a57a15990..5f645466979d 100644 --- a/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/lib/codemirror.js +++ b/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/lib/codemirror.js @@ -21,19 +21,21 @@ var platform = navigator.platform var gecko = /gecko\/\d/i.test(userAgent) var ie_upto10 = /MSIE \d/.test(userAgent) var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent) -var ie = ie_upto10 || ie_11up -var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]) -var webkit = /WebKit\//.test(userAgent) +var edge = /Edge\/(\d+)/.exec(userAgent) +var ie = ie_upto10 || ie_11up || edge +var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : +(edge || ie_11up)[1]) +var webkit = !edge && /WebKit\//.test(userAgent) var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent) -var chrome = /Chrome\//.test(userAgent) +var chrome = !edge && /Chrome\//.test(userAgent) var presto = /Opera\//.test(userAgent) var safari = /Apple Computer/.test(navigator.vendor) var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent) var phantom = /PhantomJS/.test(userAgent) -var ios = /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent) +var ios = !edge && /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent) +var android = /Android/.test(userAgent) // This is woefully incomplete. Suggestions for alternative methods welcome. -var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent) +var mobile = ios || android || /webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent) var mac = ios || /Mac/.test(platform) var chromeOS = /\bCrOS\b/.test(userAgent) var windows = /win/i.test(platform) @@ -74,6 +76,12 @@ function elt(tag, content, className, style) { else if (content) { for (var i = 0; i < content.length; ++i) { e.appendChild(content[i]) } } return e } +// wrapper for elt, which removes the elt from the accessibility tree +function eltP(tag, content, className, style) { + var e = elt(tag, content, className, style) + e.setAttribute("role", "presentation") + return e +} var range if (document.createRange) { range = function(node, start, end, endNode) { @@ -113,8 +121,8 @@ function activeElt() { } catch(e) { activeElement = document.body || null } - while (activeElement && activeElement.root && activeElement.root.activeElement) - { activeElement = activeElement.root.activeElement } + while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) + { activeElement = activeElement.shadowRoot.activeElement } return activeElement } @@ -165,11 +173,11 @@ function countColumn(string, end, tabSize, startIndex, startValue) { } } -function Delayed() {this.id = null} -Delayed.prototype.set = function(ms, f) { +var Delayed = function() {this.id = null}; +Delayed.prototype.set = function (ms, f) { clearTimeout(this.id) this.id = setTimeout(f, ms) -} +}; function indexOf(array, elt) { for (var i = 0; i < array.length; ++i) @@ -187,8 +195,7 @@ var Pass = {toString: function(){return "CodeMirror.Pass"}} // Reused option objects for setSelection & friends var sel_dontScroll = {scroll: false}; var sel_mouse = {origin: "*mouse"}; -var sel_move = {origin: "+move"} - +var sel_move = {origin: "+move"}; // The inverse of countColumn -- find the offset that corresponds to // a particular column. function findColumn(string, goal, tabSize) { @@ -264,6 +271,28 @@ function isEmpty(obj) { var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/ function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch) } +// Returns a number from the range [`0`; `str.length`] unless `pos` is outside that range. +function skipExtendingChars(str, pos, dir) { + while ((dir < 0 ? pos > 0 : pos < str.length) && isExtendingChar(str.charAt(pos))) { pos += dir } + return pos +} + +// Returns the value from the range [`from`; `to`] that satisfies +// `pred` and is closest to `from`. Assumes that at least `to` +// satisfies `pred`. Supports `from` being greater than `to`. +function findFirst(pred, from, to) { + // At any point we are certain `to` satisfies `pred`, don't know + // whether `from` does. + var dir = from > to ? -1 : 1 + for (;;) { + if (from == to) { return from } + var midF = (from + to) / 2, mid = dir < 0 ? Math.ceil(midF) : Math.floor(midF) + if (mid == from) { return pred(mid) ? from : to } + if (pred(mid)) { to = mid } + else { from = mid + dir } + } +} + // The display handles the DOM integration, both for input reading // and content drawing. It holds references to DOM nodes and // display-related state. @@ -280,7 +309,7 @@ function Display(place, doc, input) { d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler") d.gutterFiller.setAttribute("cm-not-content", "true") // Will contain the actual code, positioned to cover the viewport. - d.lineDiv = elt("div", null, "CodeMirror-code") + d.lineDiv = eltP("div", null, "CodeMirror-code") // Elements are added to these to represent selection and cursors. d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1") d.cursorDiv = elt("div", null, "CodeMirror-cursors") @@ -289,10 +318,11 @@ function Display(place, doc, input) { // When lines outside of the viewport are measured, they are drawn in this. d.lineMeasure = elt("div", null, "CodeMirror-measure") // Wraps everything that needs to exist inside the vertically-padded coordinate system - d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + d.lineSpace = eltP("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], null, "position: relative; outline: none") + var lines = eltP("div", [d.lineSpace], "CodeMirror-lines") // Moved around its parent to cover visible view. - d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative") + d.mover = elt("div", [lines], null, "position: relative") // Set to the height of the document, allowing scrolling. d.sizer = elt("div", [d.mover], "CodeMirror-sizer") d.sizerWidth = null @@ -451,15 +481,21 @@ function lineNumberFor(options, i) { } // A Pos instance represents a position within the text. -function Pos (line, ch) { - if (!(this instanceof Pos)) { return new Pos(line, ch) } - this.line = line; this.ch = ch +function Pos(line, ch, sticky) { + if ( sticky === void 0 ) sticky = null; + + if (!(this instanceof Pos)) { return new Pos(line, ch, sticky) } + this.line = line + this.ch = ch + this.sticky = sticky } // Compare two positions, return 0 if they are the same, a negative // number when a is less, and a positive number otherwise. function cmp(a, b) { return a.line - b.line || a.ch - b.ch } +function equalCursorPos(a, b) { return a.sticky == b.sticky && cmp(a, b) == 0 } + function copyPos(x) {return Pos(x.line, x.ch)} function maxPos(a, b) { return cmp(a, b) < 0 ? b : a } function minPos(a, b) { return cmp(a, b) < 0 ? a : b } @@ -487,8 +523,7 @@ function clipPosArray(doc, array) { // Optimize some code when these features are not used. var sawReadOnlySpans = false; -var sawCollapsedSpans = false - +var sawCollapsedSpans = false; function seeReadOnlySpans() { sawReadOnlySpans = true } @@ -535,7 +570,8 @@ function markedSpansBefore(old, startCh, isInsert) { var span = old[i], marker = span.marker var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh) if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { - var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)) + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh) + ;(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)) } } } return nw @@ -546,7 +582,8 @@ function markedSpansAfter(old, endCh, isInsert) { var span = old[i], marker = span.marker var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh) if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { - var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh) + ;(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, span.to == null ? null : span.to - endCh)) } } } @@ -654,7 +691,7 @@ function removeReadOnlyRanges(doc, from, to) { if (dto > 0 || !mk.inclusiveRight && !dto) { newParts.push({from: m.to, to: p.to}) } parts.splice.apply(parts, newParts) - j += newParts.length - 1 + j += newParts.length - 3 } } return parts @@ -698,7 +735,7 @@ function compareCollapsedMarkers(a, b) { // so, return the marker for that span. function collapsedSpanAtSide(line, start) { var sps = sawCollapsedSpans && line.markedSpans, found - if (sps) { for (var sp = void 0, i = 0; i < sps.length; ++i) { + if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { sp = sps[i] if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && (!found || compareCollapsedMarkers(found, sp.marker) < 0)) @@ -709,11 +746,21 @@ function collapsedSpanAtSide(line, start) { function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) } function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) } +function collapsedSpanAround(line, ch) { + var sps = sawCollapsedSpans && line.markedSpans, found + if (sps) { for (var i = 0; i < sps.length; ++i) { + var sp = sps[i] + if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) { found = sp.marker } + } } + return found +} + // Test whether there exists a collapsed span that partially // overlaps (covers the start or end, but not both) of a new span. // Such overlap is not allowed. -function conflictingCollapsedRange(doc, lineNo$$1, from, to, marker) { - var line = getLine(doc, lineNo$$1) +function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo) var sps = sawCollapsedSpans && line.markedSpans if (sps) { for (var i = 0; i < sps.length; ++i) { var sp = sps[i] @@ -739,6 +786,13 @@ function visualLine(line) { return line } +function visualLineEnd(line) { + var merged + while (merged = collapsedSpanAtEnd(line)) + { line = merged.find(1, true).line } + return line +} + // Returns an array of logical lines that continue the visual line // started by the argument, or undefined if there are no such lines. function visualLineContinued(line) { @@ -774,7 +828,7 @@ function visualLineEndNo(doc, lineN) { // they are entirely covered by collapsed, non-widget span. function lineIsHidden(doc, line) { var sps = sawCollapsedSpans && line.markedSpans - if (sps) { for (var sp = void 0, i = 0; i < sps.length; ++i) { + if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { sp = sps[i] if (!sp.marker.collapsed) { continue } if (sp.from == null) { return true } @@ -790,7 +844,7 @@ function lineIsHiddenInner(doc, line, span) { } if (span.marker.inclusiveRight && span.to == line.text.length) { return true } - for (var sp = void 0, i = 0; i < line.markedSpans.length; ++i) { + for (var sp = (void 0), i = 0; i < line.markedSpans.length; ++i) { sp = line.markedSpans[i] if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && (sp.to == null || sp.to != span.from) && @@ -858,96 +912,35 @@ function findMaxLine(cm) { // BIDI HELPERS function iterateBidiSections(order, from, to, f) { - if (!order) { return f(from, to, "ltr") } + if (!order) { return f(from, to, "ltr", 0) } 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) { - f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr") + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr", i) found = true } } if (!found) { f(from, to, "ltr") } } -function bidiLeft(part) { return part.level % 2 ? part.to : part.from } -function bidiRight(part) { return part.level % 2 ? part.from : part.to } - -function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0 } -function lineRight(line) { - var order = getOrder(line) - if (!order) { return line.text.length } - return bidiRight(lst(order)) -} - -function compareBidiLevel(order, a, b) { - var linedir = order[0].level - if (a == linedir) { return true } - if (b == linedir) { return false } - return a < b -} - var bidiOther = null -function getBidiPartAt(order, pos) { +function getBidiPartAt(order, ch, sticky) { var found bidiOther = null for (var i = 0; i < order.length; ++i) { var cur = order[i] - if (cur.from < pos && cur.to > pos) { return i } - if ((cur.from == pos || cur.to == pos)) { - if (found == null) { - found = i - } else if (compareBidiLevel(order, cur.level, order[found].level)) { - if (cur.from != cur.to) { bidiOther = found } - return i - } else { - if (cur.from != cur.to) { bidiOther = i } - return found - } + if (cur.from < ch && cur.to > ch) { return i } + if (cur.to == ch) { + if (cur.from != cur.to && sticky == "before") { found = i } + else { bidiOther = i } } - } - return found -} - -function moveInLine(line, pos, dir, byUnit) { - if (!byUnit) { return pos + dir } - do { pos += dir } - while (pos > 0 && isExtendingChar(line.text.charAt(pos))) - return pos -} - -// This is needed in order to move 'visually' through bi-directional -// text -- i.e., pressing left should make the cursor go left, even -// when in RTL text. The tricky part is the 'jumps', where RTL and -// LTR text touch each other. This often requires the cursor offset -// to move more than one unit, in order to visually move one unit. -function moveVisually(line, start, dir, byUnit) { - var bidi = getOrder(line) - if (!bidi) { return moveLogically(line, start, dir, byUnit) } - var pos = getBidiPartAt(bidi, start), part = bidi[pos] - var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit) - - 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 { - 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) } + if (cur.from == ch) { + if (cur.from != cur.to && sticky != "before") { found = i } + else { bidiOther = i } } } -} - -function moveLogically(line, start, dir, byUnit) { - var target = start + dir - if (byUnit) { while (target > 0 && isExtendingChar(line.text.charAt(target))) { target += dir } } - return target < 0 || target > line.text.length ? null : target + return found != null ? found : bidiOther } // Bidirectional ordering algorithm @@ -990,16 +983,16 @@ var bidiOrdering = (function() { var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/ var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/ - // Browsers seem to always treat the boundaries of block elements as being L. - var outerType = "L" function BidiSpan(level, from, to) { this.level = level this.from = from; this.to = to } - return function(str) { - if (!bidiRE.test(str)) { return false } + return function(str, direction) { + var outerType = direction == "ltr" ? "L" : "R" + + if (str.length == 0 || direction == "ltr" && !bidiRE.test(str)) { return false } var len = str.length, types = [] for (var i = 0; i < len; ++i) { types.push(charType(str.charCodeAt(i))) } @@ -1044,7 +1037,7 @@ var bidiOrdering = (function() { var type$3 = types[i$4] if (type$3 == ",") { types[i$4] = "N" } else if (type$3 == "%") { - var end = void 0 + var end = (void 0) for (end = i$4 + 1; end < len && types[end] == "%"; ++end) {} var replace = (i$4 && types[i$4-1] == "!") || (end < len && types[end] == "1") ? "1" : "N" for (var j = i$4; j < end; ++j) { types[j] = replace } @@ -1069,11 +1062,11 @@ var bidiOrdering = (function() { // N2. Any remaining neutrals take the embedding direction. for (var i$6 = 0; i$6 < len; ++i$6) { if (isNeutral.test(types[i$6])) { - var end$1 = void 0 + var end$1 = (void 0) for (end$1 = i$6 + 1; end$1 < len && isNeutral.test(types[end$1]); ++end$1) {} var before = (i$6 ? types[i$6-1] : outerType) == "L" var after = (end$1 < len ? types[end$1] : outerType) == "L" - var replace$1 = before || after ? "L" : "R" + var replace$1 = before == after ? (before ? "L" : "R") : outerType for (var j$1 = i$6; j$1 < end$1; ++j$1) { types[j$1] = replace$1 } i$6 = end$1 - 1 } @@ -1105,29 +1098,27 @@ var bidiOrdering = (function() { if (pos < i$7) { order.splice(at, 0, new BidiSpan(1, pos, i$7)) } } } - if (order[0].level == 1 && (m = str.match(/^\s+/))) { - order[0].from = m[0].length - order.unshift(new BidiSpan(0, 0, m[0].length)) - } - if (lst(order).level == 1 && (m = str.match(/\s+$/))) { - lst(order).to -= m[0].length - order.push(new BidiSpan(0, len - m[0].length, len)) + if (direction == "ltr") { + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length + order.unshift(new BidiSpan(0, 0, m[0].length)) + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length + order.push(new BidiSpan(0, len - m[0].length, len)) + } } - if (order[0].level == 2) - { order.unshift(new BidiSpan(1, order[0].to, order[0].to)) } - if (order[0].level != lst(order).level) - { order.push(new BidiSpan(order[0].level, len, len)) } - return order + return direction == "rtl" ? order.reverse() : order } })() // Get the bidi ordering for the given line (and cache it). Returns // false for lines that are fully left-to-right, and an array of // BidiSpan objects otherwise. -function getOrder(line) { +function getOrder(line, direction) { var order = line.order - if (order == null) { order = line.order = bidiOrdering(line.text) } + if (order == null) { order = line.order = bidiOrdering(line.text, direction) } return order } @@ -1144,8 +1135,8 @@ var on = function(emitter, type, f) { } else if (emitter.attachEvent) { emitter.attachEvent("on" + type, f) } else { - var map$$1 = emitter._handlers || (emitter._handlers = {}) - map$$1[type] = (map$$1[type] || noHandlers).concat(f) + var map = emitter._handlers || (emitter._handlers = {}) + map[type] = (map[type] || noHandlers).concat(f) } } @@ -1159,11 +1150,11 @@ function off(emitter, type, f) { } else if (emitter.detachEvent) { emitter.detachEvent("on" + type, f) } else { - var map$$1 = emitter._handlers, arr = map$$1 && map$$1[type] + var map = emitter._handlers, arr = map && map[type] if (arr) { var index = indexOf(arr, f) if (index > -1) - { map$$1[type] = arr.slice(0, index).concat(arr.slice(index + 1)) } + { map[type] = arr.slice(0, index).concat(arr.slice(index + 1)) } } } } @@ -1291,11 +1282,11 @@ var hasSelection = window.getSelection ? function (te) { try { return te.selectionStart != te.selectionEnd } catch(e) { return false } } : function (te) { - var range$$1 - try {range$$1 = te.ownerDocument.selection.createRange()} + var range + try {range = te.ownerDocument.selection.createRange()} catch(e) {} - if (!range$$1 || range$$1.parentElement() != te) { return false } - return range$$1.compareEndPoints("StartToEnd", range$$1) != 0 + if (!range || range.parentElement() != te) { return false } + return range.compareEndPoints("StartToEnd", range) != 0 } var hasCopyEvent = (function () { @@ -1314,10 +1305,8 @@ function hasBadZoomedRects(measure) { return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1 } -// Known modes, by name and by MIME var modes = {}; -var mimeModes = {} - +var mimeModes = {}; // Extra arguments are stored as the mode's dependencies, which is // used by (legacy) mechanisms like loadmode.js to automatically // load a mode. (Preferred mechanism is the require/define calls.) @@ -1415,97 +1404,156 @@ function startState(mode, a1, a2) { // Fed to the mode parsers, provides helper functions to make // parsers more succinct. -var StringStream = function(string, tabSize) { +var StringStream = function(string, tabSize, lineOracle) { this.pos = this.start = 0 this.string = string this.tabSize = tabSize || 8 this.lastColumnPos = this.lastColumnValue = 0 this.lineStart = 0 -} + this.lineOracle = lineOracle +}; -StringStream.prototype = { - eol: function() {return this.pos >= this.string.length}, - sol: function() {return this.pos == this.lineStart}, - peek: function() {return this.string.charAt(this.pos) || undefined}, - next: function() { - if (this.pos < this.string.length) - { return this.string.charAt(this.pos++) } - }, - eat: function(match) { - var ch = this.string.charAt(this.pos) - var ok - if (typeof match == "string") { ok = ch == match } - else { ok = ch && (match.test ? match.test(ch) : match(ch)) } - if (ok) {++this.pos; return ch} - }, - eatWhile: function(match) { - var start = this.pos - while (this.eat(match)){} - return this.pos > start - }, - eatSpace: function() { +StringStream.prototype.eol = function () {return this.pos >= this.string.length}; +StringStream.prototype.sol = function () {return this.pos == this.lineStart}; +StringStream.prototype.peek = function () {return this.string.charAt(this.pos) || undefined}; +StringStream.prototype.next = function () { + if (this.pos < this.string.length) + { return this.string.charAt(this.pos++) } +}; +StringStream.prototype.eat = function (match) { + var ch = this.string.charAt(this.pos) + var ok + if (typeof match == "string") { ok = ch == match } + else { ok = ch && (match.test ? match.test(ch) : match(ch)) } + if (ok) {++this.pos; return ch} +}; +StringStream.prototype.eatWhile = function (match) { + var start = this.pos + while (this.eat(match)){} + return this.pos > start +}; +StringStream.prototype.eatSpace = function () { var this$1 = this; - var start = this.pos - while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) { ++this$1.pos } - return this.pos > start - }, - skipToEnd: function() {this.pos = this.string.length}, - skipTo: function(ch) { - var found = this.string.indexOf(ch, this.pos) - if (found > -1) {this.pos = found; return true} - }, - backUp: function(n) {this.pos -= n}, - 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 - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) - }, - indentation: function() { - return countColumn(this.string, null, this.tabSize) - - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) - }, - match: function(pattern, consume, caseInsensitive) { - if (typeof pattern == "string") { - var cased = function (str) { return caseInsensitive ? str.toLowerCase() : str; } - var substr = this.string.substr(this.pos, pattern.length) - if (cased(substr) == cased(pattern)) { - if (consume !== false) { this.pos += pattern.length } - return true - } - } else { - var match = this.string.slice(this.pos).match(pattern) - if (match && match.index > 0) { return null } - if (match && consume !== false) { this.pos += match[0].length } - return match + var start = this.pos + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) { ++this$1.pos } + return this.pos > start +}; +StringStream.prototype.skipToEnd = function () {this.pos = this.string.length}; +StringStream.prototype.skipTo = function (ch) { + var found = this.string.indexOf(ch, this.pos) + if (found > -1) {this.pos = found; return true} +}; +StringStream.prototype.backUp = function (n) {this.pos -= n}; +StringStream.prototype.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 - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) +}; +StringStream.prototype.indentation = function () { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) +}; +StringStream.prototype.match = function (pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function (str) { return caseInsensitive ? str.toLowerCase() : str; } + var substr = this.string.substr(this.pos, pattern.length) + if (cased(substr) == cased(pattern)) { + if (consume !== false) { this.pos += pattern.length } + return true } - }, - current: function(){return this.string.slice(this.start, this.pos)}, - hideFirstChars: function(n, inner) { - this.lineStart += n - try { return inner() } - finally { this.lineStart -= n } + } else { + var match = this.string.slice(this.pos).match(pattern) + if (match && match.index > 0) { return null } + if (match && consume !== false) { this.pos += match[0].length } + return match } -} +}; +StringStream.prototype.current = function (){return this.string.slice(this.start, this.pos)}; +StringStream.prototype.hideFirstChars = function (n, inner) { + this.lineStart += n + try { return inner() } + finally { this.lineStart -= n } +}; +StringStream.prototype.lookAhead = function (n) { + var oracle = this.lineOracle + return oracle && oracle.lookAhead(n) +}; +StringStream.prototype.baseToken = function () { + var oracle = this.lineOracle + return oracle && oracle.baseToken(this.pos) +}; + +var SavedContext = function(state, lookAhead) { + this.state = state + this.lookAhead = lookAhead +}; + +var Context = function(doc, state, line, lookAhead) { + this.state = state + this.doc = doc + this.line = line + this.maxLookAhead = lookAhead || 0 + this.baseTokens = null + this.baseTokenPos = 1 +}; + +Context.prototype.lookAhead = function (n) { + var line = this.doc.getLine(this.line + n) + if (line != null && n > this.maxLookAhead) { this.maxLookAhead = n } + return line +}; + +Context.prototype.baseToken = function (n) { + var this$1 = this; + + if (!this.baseTokens) { return null } + while (this.baseTokens[this.baseTokenPos] <= n) + { this$1.baseTokenPos += 2 } + var type = this.baseTokens[this.baseTokenPos + 1] + return {type: type && type.replace(/( |^)overlay .*/, ""), + size: this.baseTokens[this.baseTokenPos] - n} +}; + +Context.prototype.nextLine = function () { + this.line++ + if (this.maxLookAhead > 0) { this.maxLookAhead-- } +}; + +Context.fromSaved = function (doc, saved, line) { + if (saved instanceof SavedContext) + { return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead) } + else + { return new Context(doc, copyState(doc.mode, saved), line) } +}; + +Context.prototype.save = function (copy) { + var state = copy !== false ? copyState(this.doc.mode, this.state) : this.state + return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state +}; + // Compute a style array (an array starting with a mode generation // -- for invalidation -- followed by pairs of end positions and // style strings), which is used to highlight the tokens on the // line. -function highlightLine(cm, line, state, forceToEnd) { +function highlightLine(cm, line, context, forceToEnd) { // A styles array always starts with a number identifying the // mode/overlays that it is based on (for easy invalidation). var st = [cm.state.modeGen], lineClasses = {} // Compute the base array of styles - runMode(cm, line.text, cm.doc.mode, state, function (end, style) { return st.push(end, style); }, - lineClasses, forceToEnd) + runMode(cm, line.text, cm.doc.mode, context, function (end, style) { return st.push(end, style); }, + lineClasses, forceToEnd) + var state = context.state // Run overlays, adjust style array. var loop = function ( o ) { + context.baseTokens = st var overlay = cm.state.overlays[o], i = 1, at = 0 - runMode(cm, line.text, overlay.mode, true, function (end, style) { + context.state = true + runMode(cm, line.text, overlay.mode, context, function (end, style) { var start = i // Ensure there's a token end at the current position, and that i points at it while (at < end) { @@ -1526,6 +1574,9 @@ function highlightLine(cm, line, state, forceToEnd) { } } }, lineClasses) + context.state = state + context.baseTokens = null + context.baseTokenPos = 1 }; for (var o = 0; o < cm.state.overlays.length; ++o) loop( o ); @@ -1535,43 +1586,47 @@ function highlightLine(cm, line, state, forceToEnd) { function getLineStyles(cm, line, updateFrontier) { if (!line.styles || line.styles[0] != cm.state.modeGen) { - var state = getStateBefore(cm, lineNo(line)) - var result = highlightLine(cm, line, line.text.length > cm.options.maxHighlightLength ? copyState(cm.doc.mode, state) : state) - line.stateAfter = state + var context = getContextBefore(cm, lineNo(line)) + var resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state) + var result = highlightLine(cm, line, context) + if (resetState) { context.state = resetState } + line.stateAfter = context.save(!resetState) line.styles = result.styles if (result.classes) { line.styleClasses = result.classes } else if (line.styleClasses) { line.styleClasses = null } - if (updateFrontier === cm.doc.frontier) { cm.doc.frontier++ } + if (updateFrontier === cm.doc.highlightFrontier) + { cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier) } } return line.styles } -function getStateBefore(cm, n, precise) { +function getContextBefore(cm, n, precise) { var doc = cm.doc, display = cm.display - 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) { - processLine(cm, line.text, state) - var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo - line.stateAfter = save ? copyState(doc.mode, state) : null - ++pos + if (!doc.mode.startState) { return new Context(doc, true, n) } + var start = findStartLine(cm, n, precise) + var saved = start > doc.first && getLine(doc, start - 1).stateAfter + var context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start) + + doc.iter(start, n, function (line) { + processLine(cm, line.text, context) + var pos = context.line + line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null + context.nextLine() }) - if (precise) { doc.frontier = pos } - return state + if (precise) { doc.modeFrontier = context.line } + return context } // Lightweight form of highlight -- proceed over this line and // update state, but don't save a style array. Used for lines that // aren't currently visible. -function processLine(cm, text, state, startAt) { +function processLine(cm, text, context, startAt) { var mode = cm.doc.mode - var stream = new StringStream(text, cm.options.tabSize) + var stream = new StringStream(text, cm.options.tabSize, context) stream.start = stream.pos = startAt || 0 - if (text == "") { callBlankLine(mode, state) } + if (text == "") { callBlankLine(mode, context.state) } while (!stream.eol()) { - readToken(mode, stream, state) + readToken(mode, stream, context.state) stream.start = stream.pos } } @@ -1592,26 +1647,26 @@ function readToken(mode, stream, state, inner) { throw new Error("Mode " + mode.name + " failed to advance stream.") } +var Token = function(stream, type, state) { + this.start = stream.start; this.end = stream.pos + this.string = stream.current() + this.type = type || null + this.state = state +}; + // Utility for getTokenAt and getLineTokens function takeToken(cm, pos, precise, asArray) { - var getObj = function (copy) { return ({ - start: stream.start, end: stream.pos, - string: stream.current(), - type: style || null, - state: copy ? copyState(doc.mode, state) : state - }); } - var doc = cm.doc, mode = doc.mode, style pos = clipPos(doc, pos) - var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise) - var stream = new StringStream(line.text, cm.options.tabSize), tokens + var line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise) + var stream = new StringStream(line.text, cm.options.tabSize, context), tokens if (asArray) { tokens = [] } while ((asArray || stream.pos < pos.ch) && !stream.eol()) { stream.start = stream.pos - style = readToken(mode, stream, state) - if (asArray) { tokens.push(getObj(true)) } + style = readToken(mode, stream, context.state) + if (asArray) { tokens.push(new Token(stream, style, copyState(doc.mode, context.state))) } } - return asArray ? tokens : getObj() + return asArray ? tokens : new Token(stream, style, context.state) } function extractLineClasses(type, output) { @@ -1629,21 +1684,21 @@ function extractLineClasses(type, output) { } // Run the given mode's parser over a line, calling f for each token. -function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) { +function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) { 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 + var stream = new StringStream(text, cm.options.tabSize, context), style var inner = cm.options.addModeClass && [null] - if (text == "") { extractLineClasses(callBlankLine(mode, state), lineClasses) } + if (text == "") { extractLineClasses(callBlankLine(mode, context.state), lineClasses) } while (!stream.eol()) { if (stream.pos > cm.options.maxHighlightLength) { flattenSpans = false - if (forceToEnd) { processLine(cm, text, state, stream.pos) } + if (forceToEnd) { processLine(cm, text, context, stream.pos) } stream.pos = text.length style = null } else { - style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses) + style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses) } if (inner) { var mName = inner[0].name @@ -1678,8 +1733,9 @@ function findStartLine(cm, n, precise) { 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 && (!precise || search <= doc.frontier)) { return search } + var line = getLine(doc, search - 1), after = line.stateAfter + if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier)) + { return search } var indented = countColumn(line.text, null, cm.options.tabSize) if (minline == null || minindent > indented) { minline = search - 1 @@ -1689,17 +1745,35 @@ function findStartLine(cm, n, precise) { return minline } +function retreatFrontier(doc, n) { + doc.modeFrontier = Math.min(doc.modeFrontier, n) + if (doc.highlightFrontier < n - 10) { return } + var start = doc.first + for (var line = n - 1; line > start; line--) { + var saved = getLine(doc, line).stateAfter + // change is on 3 + // state on line 1 looked ahead 2 -- so saw 3 + // test 1 + 2 < 3 should cover this + if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) { + start = line + 1 + break + } + } + doc.highlightFrontier = Math.min(doc.highlightFrontier, start) +} + // LINE DATA STRUCTURE // Line objects. These hold state related to a line, including // highlighting info (the styles array). -function Line(text, markedSpans, estimateHeight) { +var Line = function(text, markedSpans, estimateHeight) { this.text = text attachMarkedSpans(this, markedSpans) this.height = estimateHeight ? estimateHeight(this) : 1 -} +}; + +Line.prototype.lineNo = function () { return lineNo(this) }; eventMixin(Line) -Line.prototype.lineNo = function() { return lineNo(this) } // Change the content (text, markers) of a line. Automatically // invalidates cached information and tries to re-estimate the @@ -1725,7 +1799,7 @@ function cleanUpLine(line) { // containing one or more styles) to a CSS style. This is cached, // and also looks for line-wide styles. var styleToClassCache = {}; -var styleToClassCacheWithMode = {} +var styleToClassCacheWithMode = {}; function interpretTokenStyle(style, options) { if (!style || /^\s*$/.test(style)) { return null } var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache @@ -1742,8 +1816,8 @@ function buildLineContent(cm, lineView) { // The padding-right forces the element to have a 'border', which // is needed on Webkit to be able to get line-level bounding // rectangles for it (in measureChar). - var content = elt("span", null, null, webkit ? "padding-right: .1px" : null) - var builder = {pre: elt("pre", [content], "CodeMirror-line"), content: content, + var content = eltP("span", null, null, webkit ? "padding-right: .1px" : null) + var builder = {pre: eltP("pre", [content], "CodeMirror-line"), content: content, col: 0, pos: 0, cm: cm, trailingSpace: false, splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")} @@ -1751,12 +1825,12 @@ function buildLineContent(cm, lineView) { // Iterate over the logical lines that make up this visual line. for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { - var line = i ? lineView.rest[i - 1] : lineView.line, order = void 0 + var line = i ? lineView.rest[i - 1] : lineView.line, order = (void 0) builder.pos = 0 builder.addToken = buildToken // Optionally wire in some hacks into the token-rendering // algorithm, to deal with browser quirks. - if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction))) { builder.addToken = buildTokenBadBidi(builder.addToken, order) } builder.map = [] var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line) @@ -1777,7 +1851,7 @@ function buildLineContent(cm, lineView) { lineView.measure.map = builder.map lineView.measure.cache = {} } else { - (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map) + ;(lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map) ;(lineView.measure.caches || (lineView.measure.caches = [])).push({}) } } @@ -1833,7 +1907,7 @@ function buildToken(builder, text, style, startStyle, endStyle, title, css) { } if (!m) { break } pos += skipped + 1 - var txt$1 = void 0 + var txt$1 = (void 0) if (m[0] == "\t") { var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize txt$1 = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")) @@ -1888,7 +1962,7 @@ function buildTokenBadBidi(inner, order) { var start = builder.pos, end = start + text.length for (;;) { // Find the part that overlaps with the start of this text - var part = void 0 + var part = (void 0) for (var i = 0; i < order.length; i++) { part = order[i] if (part.to > start && part.from <= start) { break } @@ -1934,7 +2008,7 @@ function insertLineContent(line, builder, styles) { if (nextChange == pos) { // Update current marker set spanStyle = spanEndStyle = spanStartStyle = title = css = "" collapsed = null; nextChange = Infinity - var foundBookmarks = [], endStyles = void 0 + var foundBookmarks = [], endStyles = (void 0) for (var j = 0; j < spans.length; ++j) { var sp = spans[j], m = sp.marker if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { @@ -2097,7 +2171,7 @@ function updateLineForChanges(cm, lineView, lineN, dims) { var type = lineView.changes[j] if (type == "text") { updateLineText(cm, lineView) } else if (type == "gutter") { updateLineGutter(cm, lineView, lineN, dims) } - else if (type == "class") { updateLineClasses(lineView) } + else if (type == "class") { updateLineClasses(cm, lineView) } else if (type == "widget") { updateLineWidgets(cm, lineView, dims) } } lineView.changes = null @@ -2116,7 +2190,7 @@ function ensureLineWrapped(lineView) { return lineView.node } -function updateLineBackground(lineView) { +function updateLineBackground(cm, lineView) { var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass if (cls) { cls += " CodeMirror-linebackground" } if (lineView.background) { @@ -2125,6 +2199,7 @@ function updateLineBackground(lineView) { } else if (cls) { var wrap = ensureLineWrapped(lineView) lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild) + cm.display.input.setUneditable(lineView.background) } } @@ -2152,14 +2227,14 @@ function updateLineText(cm, lineView) { if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { lineView.bgClass = built.bgClass lineView.textClass = built.textClass - updateLineClasses(lineView) + updateLineClasses(cm, lineView) } else if (cls) { lineView.text.className = cls } } -function updateLineClasses(lineView) { - updateLineBackground(lineView) +function updateLineClasses(cm, lineView) { + updateLineBackground(cm, lineView) if (lineView.line.wrapClass) { ensureLineWrapped(lineView).className = lineView.line.wrapClass } else if (lineView.node != lineView.text) @@ -2181,6 +2256,7 @@ function updateLineGutter(cm, lineView, lineN, dims) { var wrap = ensureLineWrapped(lineView) lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px; width: " + (dims.gutterTotalWidth) + "px")) + cm.display.input.setUneditable(lineView.gutterBackground) wrap.insertBefore(lineView.gutterBackground, lineView.text) } var markers = lineView.line.gutterMarkers @@ -2207,7 +2283,7 @@ function updateLineGutter(cm, lineView, lineN, dims) { function updateLineWidgets(cm, lineView, dims) { if (lineView.alignable) { lineView.alignable = null } - for (var node = lineView.node.firstChild, next = void 0; node; node = next) { + for (var node = lineView.node.firstChild, next = (void 0); node; node = next) { next = node.nextSibling if (node.className == "CodeMirror-linewidget") { lineView.node.removeChild(node) } @@ -2222,7 +2298,7 @@ function buildLineElement(cm, lineView, lineN, dims) { if (built.bgClass) { lineView.bgClass = built.bgClass } if (built.textClass) { lineView.textClass = built.textClass } - updateLineClasses(lineView) + updateLineClasses(cm, lineView) updateLineGutter(cm, lineView, lineN, dims) insertLineWidgets(cm, lineView, dims) return lineView.node @@ -2254,7 +2330,7 @@ function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { function positionLineWidget(widget, node, lineView, dims) { if (widget.noHScroll) { - (lineView.alignable || (lineView.alignable = [])).push(node) + ;(lineView.alignable || (lineView.alignable = [])).push(node) var width = dims.wrapperWidth node.style.left = dims.fixedPos + "px" if (!widget.coverGutter) { @@ -2428,36 +2504,36 @@ function measureCharPrepared(cm, prepared, ch, bias, varHeight) { var nullRect = {left: 0, right: 0, top: 0, bottom: 0} -function nodeAndOffsetInLineMap(map$$1, ch, bias) { +function nodeAndOffsetInLineMap(map, ch, bias) { var node, start, end, collapse, mStart, mEnd // First, search the line map for the text node corresponding to, // or closest to, the target character. - for (var i = 0; i < map$$1.length; i += 3) { - mStart = map$$1[i] - mEnd = map$$1[i + 1] + for (var i = 0; i < map.length; i += 3) { + mStart = map[i] + mEnd = map[i + 1] if (ch < mStart) { start = 0; end = 1 collapse = "left" } else if (ch < mEnd) { start = ch - mStart end = start + 1 - } else if (i == map$$1.length - 3 || ch == mEnd && map$$1[i + 3] > ch) { + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { end = mEnd - mStart start = end - 1 if (ch >= mEnd) { collapse = "right" } } if (start != null) { - node = map$$1[i + 2] + node = map[i + 2] if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) { collapse = bias } if (bias == "left" && start == 0) - { while (i && map$$1[i - 2] == map$$1[i - 3] && map$$1[i - 1].insertLeft) { - node = map$$1[(i -= 3) + 2] + { while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2] collapse = "left" } } if (bias == "right" && start == mEnd - mStart) - { while (i < map$$1.length - 3 && map$$1[i + 3] == map$$1[i + 4] && !map$$1[i + 5].insertLeft) { - node = map$$1[(i += 3) + 2] + { while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2] collapse = "right" } } break @@ -2562,18 +2638,34 @@ function clearCaches(cm) { cm.display.lineNumChars = null } -function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft } -function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop } +function pageScrollX() { + // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=489206 + // which causes page_Offset and bounding client rects to use + // different reference viewports and invalidate our calculations. + if (chrome && android) { return -(document.body.getBoundingClientRect().left - parseInt(getComputedStyle(document.body).marginLeft)) } + return window.pageXOffset || (document.documentElement || document.body).scrollLeft +} +function pageScrollY() { + if (chrome && android) { return -(document.body.getBoundingClientRect().top - parseInt(getComputedStyle(document.body).marginTop)) } + return window.pageYOffset || (document.documentElement || document.body).scrollTop +} + +function widgetTopHeight(lineObj) { + var height = 0 + if (lineObj.widgets) { for (var i = 0; i < lineObj.widgets.length; ++i) { if (lineObj.widgets[i].above) + { height += widgetHeight(lineObj.widgets[i]) } } } + return height +} // Converts a {top, bottom, left, right} box from line-local // coordinates into another coordinate system. Context may be one of // "line", "div" (display.lineDiv), "local"./null (editor), "window", // or "page". function intoCoordSystem(cm, lineObj, rect, context, includeWidgets) { - if (!includeWidgets && lineObj.widgets) { for (var i = 0; i < lineObj.widgets.length; ++i) { if (lineObj.widgets[i].above) { - var size = widgetHeight(lineObj.widgets[i]) - rect.top += size; rect.bottom += size - } } } + if (!includeWidgets) { + var height = widgetTopHeight(lineObj) + rect.top += height; rect.bottom += height + } if (context == "line") { return rect } if (!context) { context = "local" } var yOff = heightAtLine(lineObj) @@ -2616,6 +2708,19 @@ function charCoords(cm, pos, context, lineObj, bias) { // Returns a box for a given cursor position, which may have an // 'other' property containing the position of the secondary cursor // on a bidi boundary. +// A cursor Pos(line, char, "before") is on the same visual line as `char - 1` +// and after `char - 1` in writing order of `char - 1` +// A cursor Pos(line, char, "after") is on the same visual line as `char` +// and before `char` in writing order of `char` +// Examples (upper-case letters are RTL, lower-case are LTR): +// Pos(0, 1, ...) +// before after +// ab a|b a|b +// aB a|B aB| +// Ab |Ab A|b +// AB B|A B|A +// Every position after the last character on a line is considered to stick +// to the last character on the line. function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { lineObj = lineObj || getLine(cm.doc, pos.line) if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj) } @@ -2624,25 +2729,24 @@ function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { 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 partPos = getBidiPartAt(order, ch) - var val = getBidi(ch, partPos) - if (bidiOther != null) { val.other = getBidi(ch, bidiOther) } + var order = getOrder(lineObj, cm.doc.direction), ch = pos.ch, sticky = pos.sticky + if (ch >= lineObj.text.length) { + ch = lineObj.text.length + sticky = "before" + } else if (ch <= 0) { + ch = 0 + sticky = "after" + } + if (!order) { return get(sticky == "before" ? ch - 1 : ch, sticky == "before") } + + function getBidi(ch, partPos, invert) { + var part = order[partPos], right = part.level == 1 + return get(invert ? ch - 1 : ch, right != invert) + } + var partPos = getBidiPartAt(order, ch, sticky) + var other = bidiOther + var val = getBidi(ch, partPos, sticky == "before") + if (other != null) { val.other = getBidi(ch, other, sticky != "before") } return val } @@ -2663,8 +2767,8 @@ function estimateCoords(cm, pos) { // the right of the character position, for example). When outside // is true, that means the coordinates lie outside the line's // vertical range. -function PosWithInfo(line, ch, outside, xRel) { - var pos = Pos(line, ch) +function PosWithInfo(line, ch, sticky, outside, xRel) { + var pos = Pos(line, ch, sticky) pos.xRel = xRel if (outside) { pos.outside = true } return pos @@ -2675,75 +2779,165 @@ function PosWithInfo(line, ch, outside, xRel) { function coordsChar(cm, x, y) { var doc = cm.doc y += cm.display.viewOffset - if (y < 0) { return PosWithInfo(doc.first, 0, true, -1) } + if (y < 0) { return PosWithInfo(doc.first, 0, null, true, -1) } var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1 if (lineN > last) - { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1) } + { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, true, 1) } if (x < 0) { x = 0 } var lineObj = getLine(doc, lineN) for (;;) { var found = coordsCharInner(cm, lineObj, lineN, x, y) - var merged = collapsedSpanAtEnd(lineObj) - var mergedPos = merged && merged.find(0, true) - if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) - { lineN = lineNo(lineObj = mergedPos.to.line) } - else - { return found } + var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 ? 1 : 0)) + if (!collapsed) { return found } + var rangeEnd = collapsed.find(1) + if (rangeEnd.line == lineN) { return rangeEnd } + lineObj = getLine(doc, lineN = rangeEnd.line) } } -function coordsCharInner(cm, lineObj, lineNo$$1, x, y) { - var innerOff = y - heightAtLine(lineObj) - var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth - var preparedMeasure = prepareMeasureForLine(cm, lineObj) +function wrappedLineExtent(cm, lineObj, preparedMeasure, y) { + y -= widgetTopHeight(lineObj) + var end = lineObj.text.length + var begin = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch - 1).bottom <= y; }, end, 0) + end = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch).top > y; }, begin, end) + return {begin: begin, end: end} +} - function getX(ch) { - var sp = cursorCoords(cm, Pos(lineNo$$1, ch), "line", lineObj, preparedMeasure) - wrongLine = true - if (innerOff > sp.bottom) { return sp.left - adjust } - else if (innerOff < sp.top) { return sp.left + adjust } - else { wrongLine = false } - return sp.left - } +function wrappedLineExtentChar(cm, lineObj, preparedMeasure, target) { + if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj) } + var targetTop = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, target), "line").top + return wrappedLineExtent(cm, lineObj, preparedMeasure, targetTop) +} - var bidi = getOrder(lineObj), dist = lineObj.text.length - var from = lineLeft(lineObj), to = lineRight(lineObj) - var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine +// Returns true if the given side of a box is after the given +// coordinates, in top-to-bottom, left-to-right order. +function boxIsAfter(box, x, y, left) { + return box.bottom <= y ? false : box.top > y ? true : (left ? box.left : box.right) > x +} - if (x > toX) { return PosWithInfo(lineNo$$1, to, toOutside, 1) } - // Do a binary search between these bounds. - for (;;) { - if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { - var ch = x < fromX || x - fromX <= toX - x ? from : to - var outside = ch == from ? fromOutside : toOutside - var xDiff = x - (ch == from ? fromX : toX) - // This is a kludge to handle the case where the coordinates - // are after a line-wrapped line. We should replace it with a - // more general handling of cursor positions around line - // breaks. (Issue #4078) - if (toOutside && !bidi && !/\s/.test(lineObj.text.charAt(ch)) && xDiff > 0 && - ch < lineObj.text.length && preparedMeasure.view.measure.heights.length > 1) { - var charSize = measureCharPrepared(cm, preparedMeasure, ch, "right") - if (innerOff <= charSize.bottom && innerOff >= charSize.top && Math.abs(x - charSize.right) < xDiff) { - outside = false - ch++ - xDiff = x - charSize.right - } - } - while (isExtendingChar(lineObj.text.charAt(ch))) { ++ch } - var pos = PosWithInfo(lineNo$$1, ch, outside, xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0) - return pos - } - var step = Math.ceil(dist / 2), middle = from + step - if (bidi) { - middle = from - for (var i = 0; i < step; ++i) { middle = moveVisually(lineObj, middle, 1) } +function coordsCharInner(cm, lineObj, lineNo, x, y) { + // Move y into line-local coordinate space + y -= heightAtLine(lineObj) + var preparedMeasure = prepareMeasureForLine(cm, lineObj) + // When directly calling `measureCharPrepared`, we have to adjust + // for the widgets at this line. + var widgetHeight = widgetTopHeight(lineObj) + var begin = 0, end = lineObj.text.length, ltr = true + + var order = getOrder(lineObj, cm.doc.direction) + // If the line isn't plain left-to-right text, first figure out + // which bidi section the coordinates fall into. + if (order) { + var part = (cm.options.lineWrapping ? coordsBidiPartWrapped : coordsBidiPart) + (cm, lineObj, lineNo, preparedMeasure, order, x, y) + ltr = part.level != 1 + // The awkward -1 offsets are needed because findFirst (called + // on these below) will treat its first bound as inclusive, + // second as exclusive, but we want to actually address the + // characters in the part's range + begin = ltr ? part.from : part.to - 1 + end = ltr ? part.to : part.from - 1 + } + + // A binary search to find the first character whose bounding box + // starts after the coordinates. If we run across any whose box wrap + // the coordinates, store that. + var chAround = null, boxAround = null + var ch = findFirst(function (ch) { + var box = measureCharPrepared(cm, preparedMeasure, ch) + box.top += widgetHeight; box.bottom += widgetHeight + if (!boxIsAfter(box, x, y, false)) { return false } + if (box.top <= y && box.left <= x) { + chAround = ch + boxAround = box } - 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} - } + return true + }, begin, end) + + var baseX, sticky, outside = false + // If a box around the coordinates was found, use that + if (boxAround) { + // Distinguish coordinates nearer to the left or right side of the box + var atLeft = x - boxAround.left < boxAround.right - x, atStart = atLeft == ltr + ch = chAround + (atStart ? 0 : 1) + sticky = atStart ? "after" : "before" + baseX = atLeft ? boxAround.left : boxAround.right + } else { + // (Adjust for extended bound, if necessary.) + if (!ltr && (ch == end || ch == begin)) { ch++ } + // To determine which side to associate with, get the box to the + // left of the character and compare it's vertical position to the + // coordinates + sticky = ch == 0 ? "after" : ch == lineObj.text.length ? "before" : + (measureCharPrepared(cm, preparedMeasure, ch - (ltr ? 1 : 0)).bottom + widgetHeight <= y) == ltr ? + "after" : "before" + // Now get accurate coordinates for this place, in order to get a + // base X position + var coords = cursorCoords(cm, Pos(lineNo, ch, sticky), "line", lineObj, preparedMeasure) + baseX = coords.left + outside = y < coords.top || y >= coords.bottom + } + + ch = skipExtendingChars(lineObj.text, ch, 1) + return PosWithInfo(lineNo, ch, sticky, outside, x - baseX) +} + +function coordsBidiPart(cm, lineObj, lineNo, preparedMeasure, order, x, y) { + // Bidi parts are sorted left-to-right, and in a non-line-wrapping + // situation, we can take this ordering to correspond to the visual + // ordering. This finds the first part whose end is after the given + // coordinates. + var index = findFirst(function (i) { + var part = order[i], ltr = part.level != 1 + return boxIsAfter(cursorCoords(cm, Pos(lineNo, ltr ? part.to : part.from, ltr ? "before" : "after"), + "line", lineObj, preparedMeasure), x, y, true) + }, 0, order.length - 1) + var part = order[index] + // If this isn't the first part, the part's start is also after + // the coordinates, and the coordinates aren't on the same line as + // that start, move one part back. + if (index > 0) { + var ltr = part.level != 1 + var start = cursorCoords(cm, Pos(lineNo, ltr ? part.from : part.to, ltr ? "after" : "before"), + "line", lineObj, preparedMeasure) + if (boxIsAfter(start, x, y, true) && start.top > y) + { part = order[index - 1] } + } + return part +} + +function coordsBidiPartWrapped(cm, lineObj, _lineNo, preparedMeasure, order, x, y) { + // In a wrapped line, rtl text on wrapping boundaries can do things + // that don't correspond to the ordering in our `order` array at + // all, so a binary search doesn't work, and we want to return a + // part that only spans one line so that the binary search in + // coordsCharInner is safe. As such, we first find the extent of the + // wrapped line, and then do a flat search in which we discard any + // spans that aren't on the line. + var ref = wrappedLineExtent(cm, lineObj, preparedMeasure, y); + var begin = ref.begin; + var end = ref.end; + if (/\s/.test(lineObj.text.charAt(end - 1))) { end-- } + var part = null, closestDist = null + for (var i = 0; i < order.length; i++) { + var p = order[i] + if (p.from >= end || p.to <= begin) { continue } + var ltr = p.level != 1 + var endX = measureCharPrepared(cm, preparedMeasure, ltr ? Math.min(end, p.to) - 1 : Math.max(begin, p.from)).right + // Weigh against spans ending before this, so that they are only + // picked if nothing ends after + var dist = endX < x ? x - endX + 1e9 : endX - x + if (!part || closestDist > dist) { + part = p + closestDist = dist + } + } + if (!part) { part = order[order.length - 1] } + // Clip the part to the wrapped line. + if (part.from < begin) { part = {from: begin, to: part.to, level: part.level} } + if (part.to > end) { part = {from: part.from, to: end, level: part.level} } + return part } var measureText @@ -2869,19 +3063,21 @@ function updateSelection(cm) { } function prepareSelection(cm, primary) { + if ( primary === void 0 ) primary = true; + var doc = cm.doc, result = {} var curFragment = result.cursors = document.createDocumentFragment() var selFragment = result.selection = document.createDocumentFragment() for (var i = 0; i < doc.sel.ranges.length; i++) { - if (primary === false && i == doc.sel.primIndex) { continue } - var range$$1 = doc.sel.ranges[i] - if (range$$1.from().line >= cm.display.viewTo || range$$1.to().line < cm.display.viewFrom) { continue } - var collapsed = range$$1.empty() + if (!primary && i == doc.sel.primIndex) { continue } + var range = doc.sel.ranges[i] + if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue } + var collapsed = range.empty() if (collapsed || cm.options.showCursorWhenSelecting) - { drawSelectionCursor(cm, range$$1.head, curFragment) } + { drawSelectionCursor(cm, range.head, curFragment) } if (!collapsed) - { drawSelectionRange(cm, range$$1, selFragment) } + { drawSelectionRange(cm, range, selFragment) } } return result } @@ -2905,12 +3101,15 @@ function drawSelectionCursor(cm, head, output) { } } +function cmpCoords(a, b) { return a.top - b.top || a.left - b.left } + // Draws the given range as a highlighted selection -function drawSelectionRange(cm, range$$1, output) { +function drawSelectionRange(cm, range, output) { var display = cm.display, doc = cm.doc var fragment = document.createDocumentFragment() var padding = paddingH(cm.display), leftSide = padding.left var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right + var docLTR = doc.direction == "ltr" function add(left, top, width, bottom) { if (top < 0) { top = 0 } @@ -2927,35 +3126,54 @@ function drawSelectionRange(cm, range$$1, output) { 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(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 = leftSide } - if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part - add(left, leftPos.top, null, leftPos.bottom) - left = leftSide - if (leftPos.bottom < rightPos.top) { add(left, leftPos.bottom, null, rightPos.top) } + function wrapX(pos, dir, side) { + var extent = wrappedLineExtentChar(cm, lineObj, null, pos) + var prop = (dir == "ltr") == (side == "after") ? "left" : "right" + var ch = side == "after" ? extent.begin : extent.end - (/\s/.test(lineObj.text.charAt(extent.end - 1)) ? 2 : 1) + return coords(ch, prop)[prop] + } + + var order = getOrder(lineObj, doc.direction) + iterateBidiSections(order, fromArg || 0, toArg == null ? lineLen : toArg, function (from, to, dir, i) { + var ltr = dir == "ltr" + var fromPos = coords(from, ltr ? "left" : "right") + var toPos = coords(to - 1, ltr ? "right" : "left") + + var openStart = fromArg == null && from == 0, openEnd = toArg == null && to == lineLen + var first = i == 0, last = !order || i == order.length - 1 + if (toPos.top - fromPos.top <= 3) { // Single line + var openLeft = (docLTR ? openStart : openEnd) && first + var openRight = (docLTR ? openEnd : openStart) && last + var left = openLeft ? leftSide : (ltr ? fromPos : toPos).left + var right = openRight ? rightSide : (ltr ? toPos : fromPos).right + add(left, fromPos.top, right - left, fromPos.bottom) + } else { // Multiple lines + var topLeft, topRight, botLeft, botRight + if (ltr) { + topLeft = docLTR && openStart && first ? leftSide : fromPos.left + topRight = docLTR ? rightSide : wrapX(from, dir, "before") + botLeft = docLTR ? leftSide : wrapX(to, dir, "after") + botRight = docLTR && openEnd && last ? rightSide : toPos.right + } else { + topLeft = !docLTR ? leftSide : wrapX(from, dir, "before") + topRight = !docLTR && openStart && first ? rightSide : fromPos.right + botLeft = !docLTR && openEnd && last ? leftSide : toPos.left + botRight = !docLTR ? rightSide : wrapX(to, dir, "after") + } + add(topLeft, fromPos.top, topRight - topLeft, fromPos.bottom) + if (fromPos.bottom < toPos.top) { add(leftSide, fromPos.bottom, null, toPos.top) } + add(botLeft, toPos.top, botRight - botLeft, toPos.bottom) } - if (toArg == null && to == lineLen) { right = rightSide } - 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 < leftSide + 1) { left = leftSide } - add(left, rightPos.top, right - left, rightPos.bottom) + + if (!start || cmpCoords(fromPos, start) < 0) { start = fromPos } + if (cmpCoords(toPos, start) < 0) { start = toPos } + if (!end || cmpCoords(fromPos, end) < 0) { end = fromPos } + if (cmpCoords(toPos, end) < 0) { end = toPos } }) return {start: start, end: end} } - var sFrom = range$$1.from(), sTo = range$$1.to() + var sFrom = range.from(), sTo = range.to() if (sFrom.line == sTo.line) { drawForLine(sFrom.line, sFrom.ch, sTo.ch) } else { @@ -3035,56 +3253,13 @@ function onBlur(cm, e) { setTimeout(function () { if (!cm.state.focused) { cm.display.shift = false } }, 150) } -// Re-align line numbers and gutter marks to compensate for -// horizontal scrolling. -function alignHorizontally(cm) { - var display = cm.display, view = display.view - if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) { return } - var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft - var gutterW = display.gutters.offsetWidth, left = comp + "px" - for (var i = 0; i < view.length; i++) { if (!view[i].hidden) { - if (cm.options.fixedGutter) { - if (view[i].gutter) - { view[i].gutter.style.left = left } - if (view[i].gutterBackground) - { view[i].gutterBackground.style.left = left } - } - var align = view[i].alignable - if (align) { for (var j = 0; j < align.length; j++) - { align[j].style.left = left } } - } } - if (cm.options.fixedGutter) - { display.gutters.style.left = (comp + gutterW) + "px" } -} - -// Used to ensure that the line number gutter is still the right -// size for the current document size. Returns true when an update -// is needed. -function maybeUpdateLineNumberWidth(cm) { - if (!cm.options.lineNumbers) { return false } - var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display - if (last.length != display.lineNumChars) { - var test = display.measure.appendChild(elt("div", [elt("div", last)], - "CodeMirror-linenumber CodeMirror-gutter-elt")) - var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW - display.lineGutter.style.width = "" - display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1 - display.lineNumWidth = display.lineNumInnerWidth + padding - display.lineNumChars = display.lineNumInnerWidth ? last.length : -1 - display.lineGutter.style.width = display.lineNumWidth + "px" - updateGutterSpace(cm) - return true - } - return false -} - // Read the actual heights of the rendered lines, and update their // stored heights to match. function updateHeightsInViewport(cm) { var display = cm.display var prevBottom = display.lineDiv.offsetTop for (var i = 0; i < display.view.length; i++) { - var cur = display.view[i], height = void 0 + var cur = display.view[i], height = (void 0) if (cur.hidden) { continue } if (ie && ie_version < 8) { var bot = cur.node.offsetTop + cur.node.offsetHeight @@ -3096,7 +3271,7 @@ function updateHeightsInViewport(cm) { } var diff = cur.line.height - height if (height < 2) { height = textHeight(display) } - if (diff > .001 || diff < -.001) { + if (diff > .005 || diff < -.005) { updateLineHeight(cur.line, height) updateWidgetHeight(cur.line) if (cur.rest) { for (var j = 0; j < cur.rest.length; j++) @@ -3108,8 +3283,10 @@ function updateHeightsInViewport(cm) { // Read and store the height of line widgets associated with the // given line. function updateWidgetHeight(line) { - if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i) - { line.widgets[i].height = line.widgets[i].node.parentNode.offsetHeight } } + if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i) { + var w = line.widgets[i], parent = w.node.parentNode + if (parent) { w.height = parent.offsetHeight } + } } } // Compute the lines that are visible in a given viewport (defaults @@ -3136,139 +3313,221 @@ function visibleLines(display, doc, viewport) { return {from: from, to: Math.max(to, from + 1)} } -// Sync the scrollable area and scrollbars, ensure the viewport -// covers the visible area. -function setScrollTop(cm, val) { - if (Math.abs(cm.doc.scrollTop - val) < 2) { return } - cm.doc.scrollTop = val - if (!gecko) { updateDisplaySimple(cm, {top: val}) } - if (cm.display.scroller.scrollTop != val) { cm.display.scroller.scrollTop = val } - cm.display.scrollbars.setScrollTop(val) - if (gecko) { updateDisplaySimple(cm) } - startWorker(cm, 100) +// Re-align line numbers and gutter marks to compensate for +// horizontal scrolling. +function alignHorizontally(cm) { + var display = cm.display, view = display.view + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) { return } + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft + var gutterW = display.gutters.offsetWidth, left = comp + "px" + for (var i = 0; i < view.length; i++) { if (!view[i].hidden) { + if (cm.options.fixedGutter) { + if (view[i].gutter) + { view[i].gutter.style.left = left } + if (view[i].gutterBackground) + { view[i].gutterBackground.style.left = left } + } + var align = view[i].alignable + if (align) { for (var j = 0; j < align.length; j++) + { align[j].style.left = left } } + } } + if (cm.options.fixedGutter) + { display.gutters.style.left = (comp + gutterW) + "px" } } -// Sync scroller and scrollbar, ensure the gutter elements are -// aligned. -function setScrollLeft(cm, val, isScroller) { - if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) { return } - val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth) - cm.doc.scrollLeft = val - alignHorizontally(cm) - if (cm.display.scroller.scrollLeft != val) { cm.display.scroller.scrollLeft = val } - cm.display.scrollbars.setScrollLeft(val) + +// Used to ensure that the line number gutter is still the right +// size for the current document size. Returns true when an update +// is needed. +function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) { return false } + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")) + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW + display.lineGutter.style.width = "" + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1 + display.lineNumWidth = display.lineNumInnerWidth + padding + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1 + display.lineGutter.style.width = display.lineNumWidth + "px" + updateGutterSpace(cm) + return true + } + return false } -// Since the delta values reported on mouse wheel events are -// unstandardized between browsers and even browser versions, and -// generally horribly unpredictable, this code starts by measuring -// the scroll effect that the first few mouse wheel events have, -// and, from that, detects the way it can convert deltas to pixel -// offsets afterwards. -// -// The reason we want to know the amount a wheel event will scroll -// is that it gives us a chance to update the display before the -// actual scrolling happens, reducing flickering. +// SCROLLING THINGS INTO VIEW -var wheelSamples = 0; -var wheelPixelsPerUnit = null -// Fill in a browser-detected starting value on browsers where we -// know one. These don't have to be accurate -- the result of them -// being wrong would just be a slight flicker on the first wheel -// scroll (if it is large enough). -if (ie) { wheelPixelsPerUnit = -.53 } -else if (gecko) { wheelPixelsPerUnit = 15 } -else if (chrome) { wheelPixelsPerUnit = -.7 } -else if (safari) { wheelPixelsPerUnit = -1/3 } +// If an editor sits on the top or bottom of the window, partially +// scrolled out of view, this ensures that the cursor is visible. +function maybeScrollWindow(cm, rect) { + if (signalDOMEvent(cm, "scrollCursorIntoView")) { return } -function wheelEventDelta(e) { - var dx = e.wheelDeltaX, dy = e.wheelDeltaY - if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) { dx = e.detail } - if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) { dy = e.detail } - else if (dy == null) { dy = e.wheelDelta } - return {x: dx, y: dy} -} -function wheelEventPixels(e) { - var delta = wheelEventDelta(e) - delta.x *= wheelPixelsPerUnit - delta.y *= wheelPixelsPerUnit - return delta + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null + if (rect.top + box.top < 0) { doScroll = true } + else if (rect.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) { doScroll = false } + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, ("position: absolute;\n top: " + (rect.top - display.viewOffset - paddingTop(cm.display)) + "px;\n height: " + (rect.bottom - rect.top + scrollGap(cm) + display.barHeight) + "px;\n left: " + (rect.left) + "px; width: " + (Math.max(2, rect.right - rect.left)) + "px;")) + cm.display.lineSpace.appendChild(scrollNode) + scrollNode.scrollIntoView(doScroll) + cm.display.lineSpace.removeChild(scrollNode) + } } -function onScrollWheel(cm, e) { - var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y - - var display = cm.display, scroll = display.scroller - // Quit if there's nothing to scroll here - var canScrollX = scroll.scrollWidth > scroll.clientWidth - var canScrollY = scroll.scrollHeight > scroll.clientHeight - if (!(dx && canScrollX || dy && canScrollY)) { return } - - // Webkit browsers on OS X abort momentum scrolls when the target - // of the scroll event is removed from the scrollable element. - // This hack (see related code in patchDisplay) makes sure the - // element is kept around. - if (dy && mac && webkit) { - outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { - for (var i = 0; i < view.length; i++) { - if (view[i].node == cur) { - cm.display.currentWheelTarget = cur - break outer - } - } +// Scroll a given position into view (immediately), verifying that +// it actually became visible (as line heights are accurately +// measured, the position of something may 'drift' during drawing). +function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) { margin = 0 } + var rect + if (!cm.options.lineWrapping && pos == end) { + // Set pos and end to the cursor positions around the character pos sticks to + // If pos.sticky == "before", that is around pos.ch - 1, otherwise around pos.ch + // If pos == Pos(_, 0, "before"), pos and end are unchanged + pos = pos.ch ? Pos(pos.line, pos.sticky == "before" ? pos.ch - 1 : pos.ch, "after") : pos + end = pos.sticky == "before" ? Pos(pos.line, pos.ch + 1, "before") : pos + } + for (var limit = 0; limit < 5; limit++) { + var changed = false + var coords = cursorCoords(cm, pos) + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end) + rect = {left: Math.min(coords.left, endCoords.left), + top: Math.min(coords.top, endCoords.top) - margin, + right: Math.max(coords.left, endCoords.left), + bottom: Math.max(coords.bottom, endCoords.bottom) + margin} + var scrollPos = calculateScrollPos(cm, rect) + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft + if (scrollPos.scrollTop != null) { + updateScrollTop(cm, scrollPos.scrollTop) + if (Math.abs(cm.doc.scrollTop - startTop) > 1) { changed = true } } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft) + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) { changed = true } + } + if (!changed) { break } } + return rect +} - // On some browsers, horizontal scrolling will cause redraws to - // happen before the gutter has been realigned, causing it to - // wriggle around in a most unseemly way. When we have an - // estimated pixels/delta value, we just handle horizontal - // scrolling entirely here. It'll be slightly off from native, but - // better than glitching out. - if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { - if (dy && canScrollY) - { setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))) } - setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))) - // Only prevent default scrolling if vertical scrolling is - // actually possible. Otherwise, it causes vertical scroll - // jitter on OSX trackpads when deltaX is small and deltaY - // is large (issue #3579) - if (!dy || (dy && canScrollY)) - { e_preventDefault(e) } - display.wheelStartX = null // Abort measurement, if in progress - return - } +// Scroll a given set of coordinates into view (immediately). +function scrollIntoView(cm, rect) { + var scrollPos = calculateScrollPos(cm, rect) + if (scrollPos.scrollTop != null) { updateScrollTop(cm, scrollPos.scrollTop) } + if (scrollPos.scrollLeft != null) { setScrollLeft(cm, scrollPos.scrollLeft) } +} - // 'Project' the visible viewport to cover the area that is being - // scrolled into view (if we know enough to estimate it). - if (dy && wheelPixelsPerUnit != null) { - var pixels = dy * wheelPixelsPerUnit - var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight - if (pixels < 0) { top = Math.max(0, top + pixels - 50) } - else { bot = Math.min(cm.doc.height, bot + pixels + 50) } - updateDisplaySimple(cm, {top: top, bottom: bot}) +// Calculate a new scroll position needed to scroll the given +// rectangle into view. Returns an object with scrollTop and +// scrollLeft properties. When these are undefined, the +// vertical/horizontal position does not need to be adjusted. +function calculateScrollPos(cm, rect) { + var display = cm.display, snapMargin = textHeight(cm.display) + if (rect.top < 0) { rect.top = 0 } + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop + var screen = displayHeight(cm), result = {} + if (rect.bottom - rect.top > screen) { rect.bottom = rect.top + screen } + var docBottom = cm.doc.height + paddingVert(display) + var atTop = rect.top < snapMargin, atBottom = rect.bottom > docBottom - snapMargin + if (rect.top < screentop) { + result.scrollTop = atTop ? 0 : rect.top + } else if (rect.bottom > screentop + screen) { + var newTop = Math.min(rect.top, (atBottom ? docBottom : rect.bottom) - screen) + if (newTop != screentop) { result.scrollTop = newTop } } - if (wheelSamples < 20) { - if (display.wheelStartX == null) { - display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop - display.wheelDX = dx; display.wheelDY = dy - setTimeout(function () { - if (display.wheelStartX == null) { return } - var movedX = scroll.scrollLeft - display.wheelStartX - var movedY = scroll.scrollTop - display.wheelStartY - var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || - (movedX && display.wheelDX && movedX / display.wheelDX) - display.wheelStartX = display.wheelStartY = null - if (!sample) { return } - wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1) - ++wheelSamples - }, 200) - } else { - display.wheelDX += dx; display.wheelDY += dy - } + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft + var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0) + var tooWide = rect.right - rect.left > screenw + if (tooWide) { rect.right = rect.left + screenw } + if (rect.left < 10) + { result.scrollLeft = 0 } + else if (rect.left < screenleft) + { result.scrollLeft = Math.max(0, rect.left - (tooWide ? 0 : 10)) } + else if (rect.right > screenw + screenleft - 3) + { result.scrollLeft = rect.right + (tooWide ? 0 : 10) - screenw } + return result +} + +// Store a relative adjustment to the scroll position in the current +// operation (to be applied when the operation finishes). +function addToScrollTop(cm, top) { + if (top == null) { return } + resolveScrollToPos(cm) + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top +} + +// Make sure that at the end of the operation the current cursor is +// shown. +function ensureCursorVisible(cm) { + resolveScrollToPos(cm) + var cur = cm.getCursor() + cm.curOp.scrollToPos = {from: cur, to: cur, margin: cm.options.cursorScrollMargin} +} + +function scrollToCoords(cm, x, y) { + if (x != null || y != null) { resolveScrollToPos(cm) } + if (x != null) { cm.curOp.scrollLeft = x } + if (y != null) { cm.curOp.scrollTop = y } +} + +function scrollToRange(cm, range) { + resolveScrollToPos(cm) + cm.curOp.scrollToPos = range +} + +// When an operation has its scrollToPos property set, and another +// scroll action is applied before the end of the operation, this +// 'simulates' scrolling that position into view in a cheap way, so +// that the effect of intermediate scroll commands is not ignored. +function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos + if (range) { + cm.curOp.scrollToPos = null + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to) + scrollToCoordsRange(cm, from, to, range.margin) } } +function scrollToCoordsRange(cm, from, to, margin) { + var sPos = calculateScrollPos(cm, { + left: Math.min(from.left, to.left), + top: Math.min(from.top, to.top) - margin, + right: Math.max(from.right, to.right), + bottom: Math.max(from.bottom, to.bottom) + margin + }) + scrollToCoords(cm, sPos.scrollLeft, sPos.scrollTop) +} + +// Sync the scrollable area and scrollbars, ensure the viewport +// covers the visible area. +function updateScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) { return } + if (!gecko) { updateDisplaySimple(cm, {top: val}) } + setScrollTop(cm, val, true) + if (gecko) { updateDisplaySimple(cm) } + startWorker(cm, 100) +} + +function setScrollTop(cm, val, forceScroll) { + val = Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, val) + if (cm.display.scroller.scrollTop == val && !forceScroll) { return } + cm.doc.scrollTop = val + cm.display.scrollbars.setScrollTop(val) + if (cm.display.scroller.scrollTop != val) { cm.display.scroller.scrollTop = val } +} + +// Sync scroller and scrollbar, ensure the gutter elements are +// aligned. +function setScrollLeft(cm, val, isScroller, forceScroll) { + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth) + if ((isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) && !forceScroll) { return } + cm.doc.scrollLeft = val + alignHorizontally(cm) + if (cm.display.scroller.scrollLeft != val) { cm.display.scroller.scrollLeft = val } + cm.display.scrollbars.setScrollLeft(val) +} + // SCROLLBARS // Prepare DOM reads needed to update the scrollbars. Done in one @@ -3289,10 +3548,11 @@ function measureForScrollbars(cm) { } } -var NativeScrollbars = function NativeScrollbars(place, scroll, cm) { +var NativeScrollbars = function(place, scroll, cm) { this.cm = cm var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar") var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar") + vert.tabIndex = horiz.tabIndex = -1 place(vert); place(horiz) on(vert, "scroll", function () { @@ -3307,7 +3567,7 @@ var NativeScrollbars = function NativeScrollbars(place, scroll, cm) { if (ie && ie_version < 8) { this.horiz.style.minHeight = this.vert.style.minWidth = "18px" } }; -NativeScrollbars.prototype.update = function update (measure) { +NativeScrollbars.prototype.update = function (measure) { var needsH = measure.scrollWidth > measure.clientWidth + 1 var needsV = measure.scrollHeight > measure.clientHeight + 1 var sWidth = measure.nativeBarWidth @@ -3330,7 +3590,7 @@ NativeScrollbars.prototype.update = function update (measure) { this.horiz.style.left = measure.barLeft + "px" var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0) this.horiz.firstChild.style.width = - (measure.scrollWidth - measure.clientWidth + totalWidth) + "px" + Math.max(0, measure.scrollWidth - measure.clientWidth + totalWidth) + "px" } else { this.horiz.style.display = "" this.horiz.firstChild.style.width = "0" @@ -3344,17 +3604,17 @@ NativeScrollbars.prototype.update = function update (measure) { return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0} }; -NativeScrollbars.prototype.setScrollLeft = function setScrollLeft$1 (pos) { +NativeScrollbars.prototype.setScrollLeft = function (pos) { if (this.horiz.scrollLeft != pos) { this.horiz.scrollLeft = pos } - if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz) } + if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz, "horiz") } }; -NativeScrollbars.prototype.setScrollTop = function setScrollTop$1 (pos) { +NativeScrollbars.prototype.setScrollTop = function (pos) { if (this.vert.scrollTop != pos) { this.vert.scrollTop = pos } - if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert) } + if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert, "vert") } }; -NativeScrollbars.prototype.zeroWidthHack = function zeroWidthHack () { +NativeScrollbars.prototype.zeroWidthHack = function () { var w = mac && !mac_geMountainLion ? "12px" : "18px" this.horiz.style.height = this.vert.style.width = w this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none" @@ -3362,35 +3622,36 @@ NativeScrollbars.prototype.zeroWidthHack = function zeroWidthHack () { this.disableVert = new Delayed }; -NativeScrollbars.prototype.enableZeroWidthBar = function enableZeroWidthBar (bar, delay) { +NativeScrollbars.prototype.enableZeroWidthBar = function (bar, delay, type) { bar.style.pointerEvents = "auto" function maybeDisable() { // To find out whether the scrollbar is still visible, we // check whether the element under the pixel in the bottom - // left corner of the scrollbar box is the scrollbar box + // right corner of the scrollbar box is the scrollbar box // itself (when the bar is still visible) or its filler child // (when the bar is hidden). If it is still visible, we keep // it enabled, if it's hidden, we disable pointer events. var box = bar.getBoundingClientRect() - var elt$$1 = document.elementFromPoint(box.left + 1, box.bottom - 1) - if (elt$$1 != bar) { bar.style.pointerEvents = "none" } + var elt = type == "vert" ? document.elementFromPoint(box.right - 1, (box.top + box.bottom) / 2) + : document.elementFromPoint((box.right + box.left) / 2, box.bottom - 1) + if (elt != bar) { bar.style.pointerEvents = "none" } else { delay.set(1000, maybeDisable) } } delay.set(1000, maybeDisable) }; -NativeScrollbars.prototype.clear = function clear () { +NativeScrollbars.prototype.clear = function () { var parent = this.horiz.parentNode parent.removeChild(this.horiz) parent.removeChild(this.vert) }; -var NullScrollbars = function NullScrollbars () {}; +var NullScrollbars = function () {}; -NullScrollbars.prototype.update = function update () { return {bottom: 0, right: 0} }; -NullScrollbars.prototype.setScrollLeft = function setScrollLeft$2 () {}; -NullScrollbars.prototype.setScrollTop = function setScrollTop$2 () {}; -NullScrollbars.prototype.clear = function clear () {}; +NullScrollbars.prototype.update = function () { return {bottom: 0, right: 0} }; +NullScrollbars.prototype.setScrollLeft = function () {}; +NullScrollbars.prototype.setScrollTop = function () {}; +NullScrollbars.prototype.clear = function () {}; function updateScrollbars(cm, measure) { if (!measure) { measure = measureForScrollbars(cm) } @@ -3444,136 +3705,12 @@ function initScrollbars(cm) { node.setAttribute("cm-not-content", "true") }, function (pos, axis) { if (axis == "horizontal") { setScrollLeft(cm, pos) } - else { setScrollTop(cm, pos) } + else { updateScrollTop(cm, pos) } }, cm) if (cm.display.scrollbars.addClass) { addClass(cm.display.wrapper, cm.display.scrollbars.addClass) } } -// SCROLLING THINGS INTO VIEW - -// If an editor sits on the top or bottom of the window, partially -// scrolled out of view, this ensures that the cursor is visible. -function maybeScrollWindow(cm, coords) { - if (signalDOMEvent(cm, "scrollCursorIntoView")) { return } - - var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null - if (coords.top + box.top < 0) { doScroll = true } - else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) { doScroll = false } - if (doScroll != null && !phantom) { - var scrollNode = elt("div", "\u200b", null, ("position: absolute;\n top: " + (coords.top - display.viewOffset - paddingTop(cm.display)) + "px;\n height: " + (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px;\n left: " + (coords.left) + "px; width: 2px;")) - cm.display.lineSpace.appendChild(scrollNode) - scrollNode.scrollIntoView(doScroll) - cm.display.lineSpace.removeChild(scrollNode) - } -} - -// Scroll a given position into view (immediately), verifying that -// it actually became visible (as line heights are accurately -// measured, the position of something may 'drift' during drawing). -function scrollPosIntoView(cm, pos, end, margin) { - if (margin == null) { margin = 0 } - var coords - for (var limit = 0; limit < 5; limit++) { - var changed = false - coords = cursorCoords(cm, pos) - 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) - if (Math.abs(cm.doc.scrollTop - startTop) > 1) { changed = true } - } - if (scrollPos.scrollLeft != null) { - setScrollLeft(cm, scrollPos.scrollLeft) - if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) { changed = true } - } - if (!changed) { break } - } - return coords -} - -// Scroll a given set of coordinates into view (immediately). -function scrollIntoView(cm, x1, y1, x2, y2) { - var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2) - if (scrollPos.scrollTop != null) { setScrollTop(cm, scrollPos.scrollTop) } - if (scrollPos.scrollLeft != null) { setScrollLeft(cm, scrollPos.scrollLeft) } -} - -// Calculate a new scroll position needed to scroll the given -// rectangle into view. Returns an object with scrollTop and -// scrollLeft properties. When these are undefined, the -// vertical/horizontal position does not need to be adjusted. -function calculateScrollPos(cm, x1, y1, x2, y2) { - var display = cm.display, snapMargin = textHeight(cm.display) - if (y1 < 0) { y1 = 0 } - var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop - var screen = displayHeight(cm), result = {} - if (y2 - y1 > screen) { y2 = y1 + 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 screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft - var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0) - var tooWide = x2 - x1 > screenw - if (tooWide) { x2 = x1 + screenw } - if (x1 < 10) - { result.scrollLeft = 0 } - else if (x1 < screenleft) - { result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10)) } - else if (x2 > screenw + screenleft - 3) - { result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw } - return result -} - -// Store a relative adjustment to the scroll position in the current -// operation (to be applied when the operation finishes). -function addToScrollPos(cm, left, top) { - if (left != null || top != null) { resolveScrollToPos(cm) } - if (left != null) - { cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left } - if (top != null) - { cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top } -} - -// Make sure that at the end of the operation the current cursor is -// shown. -function ensureCursorVisible(cm) { - resolveScrollToPos(cm) - var cur = cm.getCursor(), from = cur, to = cur - if (!cm.options.lineWrapping) { - from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur - to = Pos(cur.line, cur.ch + 1) - } - cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true} -} - -// When an operation has its scrollToPos property set, and another -// scroll action is applied before the end of the operation, this -// 'simulates' scrolling that position into view in a cheap way, so -// that the effect of intermediate scroll commands is not ignored. -function resolveScrollToPos(cm) { - var range$$1 = cm.curOp.scrollToPos - if (range$$1) { - cm.curOp.scrollToPos = null - var from = estimateCoords(cm, range$$1.from), to = estimateCoords(cm, range$$1.to) - var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), - Math.min(from.top, to.top) - range$$1.margin, - Math.max(from.right, to.right), - Math.max(from.bottom, to.bottom) + range$$1.margin) - cm.scrollTo(sPos.scrollLeft, sPos.scrollTop) - } -} - // Operations are used to wrap a series of changes to the editor // state in such a way that each change won't have to update the // cursor and display (which would be awkward, slow, and @@ -3664,7 +3801,7 @@ function endOperation_R2(op) { } if (op.updatedDisplay || op.selectionChanged) - { op.preparedSelection = display.input.prepareSelection(op.focus) } + { op.preparedSelection = display.input.prepareSelection() } } function endOperation_W2(op) { @@ -3677,7 +3814,7 @@ function endOperation_W2(op) { cm.display.maxLineChanged = false } - var takeFocus = op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus()) + var takeFocus = op.focus && op.focus == activeElt() if (op.preparedSelection) { cm.display.input.showSelection(op.preparedSelection, takeFocus) } if (op.updatedDisplay || op.startHeight != cm.doc.height) @@ -3702,22 +3839,14 @@ function endOperation_finish(op) { { display.wheelStartX = display.wheelStartY = null } // Propagate the scroll position to the actual DOM scroller - if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) { - doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)) - display.scrollbars.setScrollTop(doc.scrollTop) - display.scroller.scrollTop = doc.scrollTop - } - if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { - doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, op.scrollLeft)) - display.scrollbars.setScrollLeft(doc.scrollLeft) - display.scroller.scrollLeft = doc.scrollLeft - alignHorizontally(cm) - } + if (op.scrollTop != null) { setScrollTop(cm, op.scrollTop, op.forceScroll) } + + if (op.scrollLeft != null) { setScrollLeft(cm, op.scrollLeft, true, true) } // If we need to scroll a specific position into view, do so. if (op.scrollToPos) { - var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), - clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin) - if (op.scrollToPos.isCursor && cm.state.focused) { maybeScrollWindow(cm, coords) } + var rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin) + maybeScrollWindow(cm, rect) } // Fire events for markers that are hidden/unidden by editing or @@ -3925,22 +4054,23 @@ function countDirtyView(cm) { // HIGHLIGHT WORKER function startWorker(cm, time) { - if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) + if (cm.doc.highlightFrontier < cm.display.viewTo) { cm.state.highlight.set(time, bind(highlightWorker, cm)) } } function highlightWorker(cm) { var doc = cm.doc - if (doc.frontier < doc.first) { doc.frontier = doc.first } - if (doc.frontier >= cm.display.viewTo) { return } + if (doc.highlightFrontier >= cm.display.viewTo) { return } var end = +new Date + cm.options.workTime - var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)) + var context = getContextBefore(cm, doc.highlightFrontier) var changedLines = [] - doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function (line) { - if (doc.frontier >= cm.display.viewFrom) { // Visible - var oldStyles = line.styles, tooLong = line.text.length > cm.options.maxHighlightLength - var highlighted = highlightLine(cm, line, tooLong ? copyState(doc.mode, state) : state, true) + doc.iter(context.line, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function (line) { + if (context.line >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles + var resetState = line.text.length > cm.options.maxHighlightLength ? copyState(doc.mode, context.state) : null + var highlighted = highlightLine(cm, line, context, true) + if (resetState) { context.state = resetState } line.styles = highlighted.styles var oldCls = line.styleClasses, newCls = highlighted.classes if (newCls) { line.styleClasses = newCls } @@ -3948,19 +4078,22 @@ function highlightWorker(cm) { var ischange = !oldStyles || oldStyles.length != line.styles.length || oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass) for (var i = 0; !ischange && i < oldStyles.length; ++i) { ischange = oldStyles[i] != line.styles[i] } - if (ischange) { changedLines.push(doc.frontier) } - line.stateAfter = tooLong ? state : copyState(doc.mode, state) + if (ischange) { changedLines.push(context.line) } + line.stateAfter = context.save() + context.nextLine() } else { if (line.text.length <= cm.options.maxHighlightLength) - { processLine(cm, line.text, state) } - line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null + { processLine(cm, line.text, context) } + line.stateAfter = context.line % 5 == 0 ? context.save() : null + context.nextLine() } - ++doc.frontier if (+new Date > end) { startWorker(cm, cm.options.workDelay) return true } }) + doc.highlightFrontier = context.line + doc.modeFrontier = Math.max(doc.modeFrontier, context.line) if (changedLines.length) { runInOp(cm, function () { for (var i = 0; i < changedLines.length; i++) { regLineChange(cm, changedLines[i], "text") } @@ -3969,7 +4102,7 @@ function highlightWorker(cm) { // DISPLAY DRAWING -var DisplayUpdate = function DisplayUpdate(cm, viewport, force) { +var DisplayUpdate = function(cm, viewport, force) { var display = cm.display this.viewport = viewport @@ -3984,11 +4117,11 @@ var DisplayUpdate = function DisplayUpdate(cm, viewport, force) { this.events = [] }; -DisplayUpdate.prototype.signal = function signal$1 (emitter, type) { +DisplayUpdate.prototype.signal = function (emitter, type) { if (hasHandler(emitter, type)) { this.events.push(arguments) } }; -DisplayUpdate.prototype.finish = function finish () { +DisplayUpdate.prototype.finish = function () { var this$1 = this; for (var i = 0; i < this.events.length; i++) @@ -4006,6 +4139,36 @@ function maybeClipScrollbars(cm) { } } +function selectionSnapshot(cm) { + if (cm.hasFocus()) { return null } + var active = activeElt() + if (!active || !contains(cm.display.lineDiv, active)) { return null } + var result = {activeElt: active} + if (window.getSelection) { + var sel = window.getSelection() + if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) { + result.anchorNode = sel.anchorNode + result.anchorOffset = sel.anchorOffset + result.focusNode = sel.focusNode + result.focusOffset = sel.focusOffset + } + } + return result +} + +function restoreSelection(snapshot) { + if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) { return } + snapshot.activeElt.focus() + if (snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) { + var sel = window.getSelection(), range = document.createRange() + range.setEnd(snapshot.anchorNode, snapshot.anchorOffset) + range.collapse(false) + sel.removeAllRanges() + sel.addRange(range) + sel.extend(snapshot.focusNode, snapshot.focusOffset) + } +} + // Does the actual updating of the line display. Bails out // (returning false) when there is nothing to be done and forced is // false. @@ -4055,14 +4218,14 @@ function updateDisplayIfNeeded(cm, update) { // For big changes, we hide the enclosing element during the // update, since that speeds up the operations on most browsers. - var focused = activeElt() + var selSnapshot = selectionSnapshot(cm) if (toUpdate > 4) { display.lineDiv.style.display = "none" } patchDisplay(cm, display.updateLineNumbers, update.dims) if (toUpdate > 4) { display.lineDiv.style.display = "" } display.renderedView = display.view // There might have been a widget with a focused element that got // hidden or updated, if so re-focus it. - if (focused && activeElt() != focused && focused.offsetHeight) { focused.focus() } + restoreSelection(selSnapshot) // Prevent selection and cursors from interfering with the scroll // width and height. @@ -4101,6 +4264,7 @@ function postUpdateDisplay(cm, update) { updateSelection(cm) updateScrollbars(cm, barMeasure) setDocumentHeight(cm, barMeasure) + update.force = false } update.signal(cm, "update", cm) @@ -4210,68 +4374,166 @@ function setGuttersForLineNumbers(options) { } } +var wheelSamples = 0; +var wheelPixelsPerUnit = null; +// Fill in a browser-detected starting value on browsers where we +// know one. These don't have to be accurate -- the result of them +// being wrong would just be a slight flicker on the first wheel +// scroll (if it is large enough). +if (ie) { wheelPixelsPerUnit = -.53 } +else if (gecko) { wheelPixelsPerUnit = 15 } +else if (chrome) { wheelPixelsPerUnit = -.7 } +else if (safari) { wheelPixelsPerUnit = -1/3 } + +function wheelEventDelta(e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) { dx = e.detail } + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) { dy = e.detail } + else if (dy == null) { dy = e.wheelDelta } + return {x: dx, y: dy} +} +function wheelEventPixels(e) { + var delta = wheelEventDelta(e) + delta.x *= wheelPixelsPerUnit + delta.y *= wheelPixelsPerUnit + return delta +} + +function onScrollWheel(cm, e) { + var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y + + var display = cm.display, scroll = display.scroller + // Quit if there's nothing to scroll here + var canScrollX = scroll.scrollWidth > scroll.clientWidth + var canScrollY = scroll.scrollHeight > scroll.clientHeight + if (!(dx && canScrollX || dy && canScrollY)) { return } + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur + break outer + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { + if (dy && canScrollY) + { updateScrollTop(cm, Math.max(0, scroll.scrollTop + dy * wheelPixelsPerUnit)) } + setScrollLeft(cm, Math.max(0, scroll.scrollLeft + dx * wheelPixelsPerUnit)) + // Only prevent default scrolling if vertical scrolling is + // actually possible. Otherwise, it causes vertical scroll + // jitter on OSX trackpads when deltaX is small and deltaY + // is large (issue #3579) + if (!dy || (dy && canScrollY)) + { e_preventDefault(e) } + display.wheelStartX = null // Abort measurement, if in progress + return + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight + if (pixels < 0) { top = Math.max(0, top + pixels - 50) } + else { bot = Math.min(cm.doc.height, bot + pixels + 50) } + updateDisplaySimple(cm, {top: top, bottom: bot}) + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop + display.wheelDX = dx; display.wheelDY = dy + setTimeout(function () { + if (display.wheelStartX == null) { return } + var movedX = scroll.scrollLeft - display.wheelStartX + var movedY = scroll.scrollTop - display.wheelStartY + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX) + display.wheelStartX = display.wheelStartY = null + if (!sample) { return } + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1) + ++wheelSamples + }, 200) + } else { + display.wheelDX += dx; display.wheelDY += dy + } + } +} + // Selection objects are immutable. A new one is created every time // the selection changes. A selection is one or more non-overlapping // (and non-touching) ranges, sorted, and an integer that indicates // which one is the primary selection (the one that's scrolled into // view, that getCursor returns, etc). -function Selection(ranges, primIndex) { +var Selection = function(ranges, primIndex) { this.ranges = ranges this.primIndex = primIndex -} +}; -Selection.prototype = { - primary: function() { return this.ranges[this.primIndex] }, - equals: function(other) { +Selection.prototype.primary = function () { return this.ranges[this.primIndex] }; + +Selection.prototype.equals = function (other) { var this$1 = this; - if (other == this) { return true } - if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) { return false } - for (var i = 0; i < this.ranges.length; i++) { - var here = this$1.ranges[i], there = other.ranges[i] - if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) { return false } - } - return true - }, - deepCopy: function() { + if (other == this) { return true } + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) { return false } + for (var i = 0; i < this.ranges.length; i++) { + var here = this$1.ranges[i], there = other.ranges[i] + if (!equalCursorPos(here.anchor, there.anchor) || !equalCursorPos(here.head, there.head)) { return false } + } + return true +}; + +Selection.prototype.deepCopy = function () { var this$1 = this; - var out = [] - for (var i = 0; i < this.ranges.length; i++) - { out[i] = new Range(copyPos(this$1.ranges[i].anchor), copyPos(this$1.ranges[i].head)) } - return new Selection(out, this.primIndex) - }, - somethingSelected: function() { + var out = [] + for (var i = 0; i < this.ranges.length; i++) + { out[i] = new Range(copyPos(this$1.ranges[i].anchor), copyPos(this$1.ranges[i].head)) } + return new Selection(out, this.primIndex) +}; + +Selection.prototype.somethingSelected = function () { var this$1 = this; - for (var i = 0; i < this.ranges.length; i++) - { if (!this$1.ranges[i].empty()) { return true } } - return false - }, - contains: function(pos, end) { + for (var i = 0; i < this.ranges.length; i++) + { if (!this$1.ranges[i].empty()) { return true } } + return false +}; + +Selection.prototype.contains = function (pos, end) { var this$1 = this; - if (!end) { end = pos } - for (var i = 0; i < this.ranges.length; i++) { - var range = this$1.ranges[i] - if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) - { return i } - } - return -1 + if (!end) { end = pos } + for (var i = 0; i < this.ranges.length; i++) { + var range = this$1.ranges[i] + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + { return i } } -} + return -1 +}; -function Range(anchor, head) { +var Range = function(anchor, head) { this.anchor = anchor; this.head = head -} +}; -Range.prototype = { - from: function() { return minPos(this.anchor, this.head) }, - to: function() { return maxPos(this.anchor, this.head) }, - empty: function() { - return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch - } -} +Range.prototype.from = function () { return minPos(this.anchor, this.head) }; +Range.prototype.to = function () { return maxPos(this.anchor, this.head) }; +Range.prototype.empty = function () { return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch }; // Take an unsorted, potentially overlapping set of ranges, and // build a selection out of it. 'Consumes' ranges array (modifying @@ -4365,7 +4627,7 @@ function resetModeState(cm) { if (line.stateAfter) { line.stateAfter = null } if (line.styles) { line.styles = null } }) - cm.doc.frontier = cm.doc.first + cm.doc.modeFrontier = cm.doc.highlightFrontier = cm.doc.first startWorker(cm, 100) cm.state.modeGen++ if (cm.curOp) { regChange(cm) } @@ -4382,16 +4644,16 @@ function isWholeLineUpdate(doc, change) { } // Perform a change on the document data structure. -function updateDoc(doc, change, markedSpans, estimateHeight$$1) { +function updateDoc(doc, change, markedSpans, estimateHeight) { function spansFor(n) {return markedSpans ? markedSpans[n] : null} function update(line, text, spans) { - updateLine(line, text, spans, estimateHeight$$1) + updateLine(line, text, spans, estimateHeight) signalLater(line, "change", line, change) } function linesFor(start, end) { var result = [] for (var i = start; i < end; ++i) - { result.push(new Line(text[i], spansFor(i), estimateHeight$$1)) } + { result.push(new Line(text[i], spansFor(i), estimateHeight)) } return result } @@ -4415,7 +4677,7 @@ function updateDoc(doc, change, markedSpans, estimateHeight$$1) { update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans) } else { var added$1 = linesFor(1, text.length - 1) - added$1.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight$$1)) + added$1.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$1) } @@ -4455,11 +4717,23 @@ function attachDoc(cm, doc) { doc.cm = cm estimateLineHeights(cm) loadMode(cm) + setDirectionClass(cm) if (!cm.options.lineWrapping) { findMaxLine(cm) } cm.options.mode = doc.modeOption regChange(cm) } +function setDirectionClass(cm) { + ;(cm.doc.direction == "rtl" ? addClass : rmClass)(cm.display.lineDiv, "CodeMirror-rtl") +} + +function directionChanged(cm) { + runInOp(cm, function () { + setDirectionClass(cm) + regChange(cm) + }) +} + function History(startGen) { // Arrays of change events and selections. Doing something adds an // event to done and clears undo. Undoing moves events from done @@ -4519,7 +4793,7 @@ function addChangeToHistory(doc, change, selAfter, opId) { if ((hist.lastOp == opId || hist.lastOrigin == change.origin && change.origin && - ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || + ((change.origin.charAt(0) == "+" && hist.lastModTime > time - (doc.cm ? doc.cm.options.historyEventDelay : 500)) || change.origin.charAt(0) == "*")) && (cur = lastChangeEvent(hist, hist.lastOp == opId))) { // Merge this change into the last event @@ -4666,7 +4940,7 @@ function copyHistoryArray(events, newGroup, instantiateSel) { var changes = event.changes, newChanges = [] copy.push({changes: newChanges}) for (var j = 0; j < changes.length; ++j) { - var change = changes[j], m = void 0 + var change = changes[j], m = (void 0) newChanges.push({from: change.from, to: change.to, text: change.text}) if (newGroup) { for (var prop in change) { if (m = prop.match(/^spans_(\d+)$/)) { if (indexOf(newGroup, Number(m[1])) > -1) { @@ -4687,8 +4961,8 @@ function copyHistoryArray(events, newGroup, instantiateSel) { // include a given position (and optionally a second position). // Otherwise, simply returns the range between the given positions. // Used for cursor motion and such. -function extendRange(doc, range, head, other) { - if (doc.cm && doc.cm.display.shift || doc.extend) { +function extendRange(range, head, other, extend) { + if (extend) { var anchor = range.anchor if (other) { var posBefore = cmp(head, anchor) < 0 @@ -4706,16 +4980,18 @@ function extendRange(doc, range, head, other) { } // Extend the primary selection range, discard the rest. -function extendSelection(doc, head, other, options) { - setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options) +function extendSelection(doc, head, other, options, extend) { + if (extend == null) { extend = doc.cm && (doc.cm.display.shift || doc.extend) } + setSelection(doc, new Selection([extendRange(doc.sel.primary(), head, other, extend)], 0), options) } // Extend all selections (pos is an array of selections with length // equal the number of selections) function extendSelections(doc, heads, options) { var out = [] + var extend = doc.cm && (doc.cm.display.shift || doc.extend) for (var i = 0; i < doc.sel.ranges.length; i++) - { out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null) } + { out[i] = extendRange(doc.sel.ranges[i], heads[i], null, extend) } var newSel = normalizeSelection(out, doc.sel.primIndex) setSelection(doc, newSel, options) } @@ -4796,7 +5072,7 @@ function setSelectionInner(doc, sel) { // Verify that the selection does not partially select any atomic // marked ranges. function reCheckSelection(doc) { - setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll) + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false)) } // Return a selection that does not partially select any atomic @@ -4832,7 +5108,7 @@ function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { if (!m.atomic) { continue } if (oldPos) { - var near = m.find(dir < 0 ? 1 : -1), diff = void 0 + var near = m.find(dir < 0 ? 1 : -1), diff = (void 0) if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft) { near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null) } if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) @@ -4921,7 +5197,7 @@ function makeChange(doc, change, ignoreReadOnly) { var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to) if (split) { for (var i = split.length - 1; i >= 0; --i) - { makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}) } + { makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin}) } } else { makeChangeInner(doc, change) } @@ -4946,7 +5222,8 @@ function makeChangeInner(doc, change) { // Revert a change stored in a document's history. function makeChangeFromHistory(doc, type, allowSelectionOnly) { - if (doc.cm && doc.cm.state.suppressEdits && !allowSelectionOnly) { return } + var suppress = doc.cm && doc.cm.state.suppressEdits + if (suppress && !allowSelectionOnly) { return } var hist = doc.history, event, selAfter = doc.sel var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done @@ -4971,8 +5248,10 @@ function makeChangeFromHistory(doc, type, allowSelectionOnly) { return } selAfter = event - } - else { break } + } else if (suppress) { + source.push(event) + return + } else { break } } // Build up a reverse change object to add to the opposite history @@ -5099,8 +5378,7 @@ function makeChangeSingleDocInEditor(cm, change, spans) { if (recomputeMaxLength) { cm.curOp.updateMaxLine = true } } - // Adjust frontier, schedule worker - doc.frontier = Math.min(doc.frontier, from.line) + retreatFrontier(doc, from.line) startWorker(cm, 400) var lendiff = change.text.length - (to.line - from.line) - 1 @@ -5128,7 +5406,8 @@ function makeChangeSingleDocInEditor(cm, change, spans) { function replaceRange(doc, code, from, to, origin) { if (!to) { to = from } - if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp } + if (cmp(to, from) < 0) { var assign; + (assign = [to, from], from = assign[0], to = assign[1], assign) } if (typeof code == "string") { code = doc.splitLines(code) } makeChange(doc, {from: from, to: to, text: code, origin: origin}) } @@ -5224,9 +5503,10 @@ function LeafChunk(lines) { } LeafChunk.prototype = { - chunkSize: function() { return this.lines.length }, + chunkSize: function chunkSize() { return this.lines.length }, + // Remove the n lines at offset 'at'. - removeInner: function(at, n) { + removeInner: function removeInner(at, n) { var this$1 = this; for (var i = at, e = at + n; i < e; ++i) { @@ -5237,21 +5517,24 @@ LeafChunk.prototype = { } this.lines.splice(at, n) }, + // Helper used to collapse a small branch into a single leaf. - collapse: function(lines) { + collapse: function collapse(lines) { lines.push.apply(lines, this.lines) }, + // Insert the given array of lines at offset 'at', count them as // having the given height. - insertInner: function(at, lines, height) { + insertInner: function insertInner(at, lines, height) { var this$1 = this; this.height += height this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)) for (var i = 0; i < lines.length; ++i) { lines[i].parent = this$1 } }, + // Used to iterate over a part of the tree. - iterN: function(at, n, op) { + iterN: function iterN(at, n, op) { var this$1 = this; for (var e = at + n; at < e; ++at) @@ -5275,8 +5558,9 @@ function BranchChunk(children) { } BranchChunk.prototype = { - chunkSize: function() { return this.size }, - removeInner: function(at, n) { + chunkSize: function chunkSize() { return this.size }, + + removeInner: function removeInner(at, n) { var this$1 = this; this.size -= n @@ -5301,12 +5585,14 @@ BranchChunk.prototype = { this.children[0].parent = this } }, - collapse: function(lines) { + + collapse: function collapse(lines) { var this$1 = this; for (var i = 0; i < this.children.length; ++i) { this$1.children[i].collapse(lines) } }, - insertInner: function(at, lines, height) { + + insertInner: function insertInner(at, lines, height) { var this$1 = this; this.size += lines.length @@ -5333,8 +5619,9 @@ BranchChunk.prototype = { at -= sz } }, + // When a node has grown, check whether it should be split. - maybeSpill: function() { + maybeSpill: function maybeSpill() { if (this.children.length <= 10) { return } var me = this do { @@ -5355,7 +5642,8 @@ BranchChunk.prototype = { } while (me.children.length > 10) me.parent.maybeSpill() }, - iterN: function(at, n, op) { + + iterN: function iterN(at, n, op) { var this$1 = this; for (var i = 0; i < this.children.length; ++i) { @@ -5372,23 +5660,17 @@ BranchChunk.prototype = { // Line widgets are block elements displayed above or below a line. -function LineWidget(doc, node, options) { +var LineWidget = function(doc, node, options) { var this$1 = this; if (options) { for (var opt in options) { if (options.hasOwnProperty(opt)) { this$1[opt] = options[opt] } } } this.doc = doc this.node = node -} -eventMixin(LineWidget) - -function adjustScrollWhenAboveVisible(cm, line, diff) { - if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) - { addToScrollPos(cm, null, diff) } -} +}; -LineWidget.prototype.clear = function() { - var this$1 = this; +LineWidget.prototype.clear = function () { + var this$1 = this; var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line) if (no == null || !ws) { return } @@ -5396,21 +5678,36 @@ LineWidget.prototype.clear = function() { if (!ws.length) { line.widgets = null } var height = widgetHeight(this) updateLineHeight(line, Math.max(0, line.height - height)) - if (cm) { runInOp(cm, function () { - adjustScrollWhenAboveVisible(cm, line, -height) - regLineChange(cm, no, "widget") - }) } -} -LineWidget.prototype.changed = function() { + if (cm) { + runInOp(cm, function () { + adjustScrollWhenAboveVisible(cm, line, -height) + regLineChange(cm, no, "widget") + }) + signalLater(cm, "lineWidgetCleared", cm, this, no) + } +}; + +LineWidget.prototype.changed = function () { + var this$1 = this; + var oldH = this.height, cm = this.doc.cm, line = this.line this.height = null var diff = widgetHeight(this) - oldH if (!diff) { return } updateLineHeight(line, line.height + diff) - if (cm) { runInOp(cm, function () { - cm.curOp.forceUpdate = true - adjustScrollWhenAboveVisible(cm, line, diff) - }) } + if (cm) { + runInOp(cm, function () { + cm.curOp.forceUpdate = true + adjustScrollWhenAboveVisible(cm, line, diff) + signalLater(cm, "lineWidgetChanged", cm, this$1, lineNo(line)) + }) + } +}; +eventMixin(LineWidget) + +function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + { addToScrollTop(cm, diff) } } function addLineWidget(doc, handle, node, options) { @@ -5425,11 +5722,12 @@ function addLineWidget(doc, handle, node, options) { if (cm && !lineIsHidden(doc, line)) { var aboveVisible = heightAtLine(line) < doc.scrollTop updateLineHeight(line, line.height + widgetHeight(widget)) - if (aboveVisible) { addToScrollPos(cm, null, widget.height) } + if (aboveVisible) { addToScrollTop(cm, widget.height) } cm.curOp.forceUpdate = true } return true }) + if (cm) { signalLater(cm, "lineWidgetAdded", cm, widget, typeof handle == "number" ? handle : lineNo(handle)) } return widget } @@ -5450,17 +5748,16 @@ function addLineWidget(doc, handle, node, options) { // when they overlap (they may nest, but not partially overlap). var nextMarkerId = 0 -function TextMarker(doc, type) { +var TextMarker = function(doc, type) { this.lines = [] this.type = type this.doc = doc this.id = ++nextMarkerId -} -eventMixin(TextMarker) +}; // Clear the marker. -TextMarker.prototype.clear = function() { - var this$1 = this; +TextMarker.prototype.clear = function () { + var this$1 = this; if (this.explicitlyCleared) { return } var cm = this.doc.cm, withOp = cm && !cm.curOp @@ -5498,18 +5795,18 @@ TextMarker.prototype.clear = function() { this.doc.cantEdit = false if (cm) { reCheckSelection(cm.doc) } } - if (cm) { signalLater(cm, "markerCleared", cm, this) } + if (cm) { signalLater(cm, "markerCleared", cm, this, min, max) } if (withOp) { endOperation(cm) } if (this.parent) { this.parent.clear() } -} +}; // Find the position of the marker in the document. Returns a {from, // to} object by default. Side can be passed to get a specific side // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the // Pos objects returned contain a line object, rather than a line // number (used to prevent looking up the same line twice). -TextMarker.prototype.find = function(side, lineObj) { - var this$1 = this; +TextMarker.prototype.find = function (side, lineObj) { + var this$1 = this; if (side == null && this.type == "bookmark") { side = 1 } var from, to @@ -5526,11 +5823,13 @@ TextMarker.prototype.find = function(side, lineObj) { } } return from && {from: from, to: to} -} +}; // Signals that the marker's widget changed, and surrounding layout // should be recomputed. -TextMarker.prototype.changed = function() { +TextMarker.prototype.changed = function () { + var this$1 = this; + var pos = this.find(-1, true), widget = this, cm = this.doc.cm if (!pos || !cm) { return } runInOp(cm, function () { @@ -5548,23 +5847,27 @@ TextMarker.prototype.changed = function() { if (dHeight) { updateLineHeight(line, line.height + dHeight) } } + signalLater(cm, "markerChanged", cm, this$1) }) -} +}; -TextMarker.prototype.attachLine = function(line) { +TextMarker.prototype.attachLine = function (line) { if (!this.lines.length && this.doc.cm) { var op = this.doc.cm.curOp if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) { (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this) } } this.lines.push(line) -} -TextMarker.prototype.detachLine = function(line) { +}; + +TextMarker.prototype.detachLine = function (line) { this.lines.splice(indexOf(this.lines, line), 1) if (!this.lines.length && this.doc.cm) { - var op = this.doc.cm.curOp;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this) + var op = this.doc.cm.curOp + ;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this) } -} +}; +eventMixin(TextMarker) // Create a marker, wire it up to the right lines, and function markText(doc, from, to, options, type) { @@ -5583,7 +5886,7 @@ function markText(doc, from, to, options, type) { if (marker.replacedWith) { // Showing up as a widget implies collapsed (widget replaces text) marker.collapsed = true - marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget") + marker.widgetNode = eltP("span", [marker.replacedWith], "CodeMirror-widget") if (!options.handleMouseEvents) { marker.widgetNode.setAttribute("cm-ignore-events", "true") } if (options.insertLeft) { marker.widgetNode.insertLeft = true } } @@ -5641,28 +5944,29 @@ function markText(doc, from, to, options, type) { // A shared marker spans multiple linked documents. It is // implemented as a meta-marker-object controlling multiple normal // markers. -function SharedTextMarker(markers, primary) { +var SharedTextMarker = function(markers, primary) { var this$1 = this; this.markers = markers this.primary = primary for (var i = 0; i < markers.length; ++i) { markers[i].parent = this$1 } -} -eventMixin(SharedTextMarker) +}; -SharedTextMarker.prototype.clear = function() { - var this$1 = this; +SharedTextMarker.prototype.clear = function () { + var this$1 = this; if (this.explicitlyCleared) { return } this.explicitlyCleared = true for (var i = 0; i < this.markers.length; ++i) { this$1.markers[i].clear() } signalLater(this, "clear") -} -SharedTextMarker.prototype.find = function(side, lineObj) { +}; + +SharedTextMarker.prototype.find = function (side, lineObj) { return this.primary.find(side, lineObj) -} +}; +eventMixin(SharedTextMarker) function markTextShared(doc, from, to, options, type) { options = copyObj(options) @@ -5712,8 +6016,8 @@ function detachSharedMarkers(markers) { } var nextDocId = 0 -var Doc = function(text, mode, firstLine, lineSep) { - if (!(this instanceof Doc)) { return new Doc(text, mode, firstLine, lineSep) } +var Doc = function(text, mode, firstLine, lineSep, direction) { + if (!(this instanceof Doc)) { return new Doc(text, mode, firstLine, lineSep, direction) } if (firstLine == null) { firstLine = 0 } BranchChunk.call(this, [new LeafChunk([new Line("", null)])]) @@ -5721,13 +6025,14 @@ var Doc = function(text, mode, firstLine, lineSep) { this.scrollTop = this.scrollLeft = 0 this.cantEdit = false this.cleanGeneration = 1 - this.frontier = firstLine + this.modeFrontier = this.highlightFrontier = firstLine var start = Pos(firstLine, 0) this.sel = simpleSelection(start) this.history = new History(null) this.id = ++nextDocId this.modeOption = mode this.lineSep = lineSep + this.direction = (direction == "rtl") ? "rtl" : "ltr" this.extend = false if (typeof text == "string") { text = this.splitLines(text) } @@ -5766,7 +6071,8 @@ Doc.prototype = createObj(BranchChunk.prototype, { var top = Pos(this.first, 0), last = this.first + this.size - 1 makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), text: this.splitLines(code), origin: "setValue", full: true}, true) - setSelection(this, simpleSelection(top)) + if (this.cm) { scrollToCoords(this.cm, 0, 0) } + setSelection(this, simpleSelection(top), sel_dontScroll) }), replaceRange: function(code, from, to, origin) { from = clipPos(this, from) @@ -5796,11 +6102,11 @@ Doc.prototype = createObj(BranchChunk.prototype, { clipPos: function(pos) {return clipPos(this, pos)}, getCursor: function(start) { - var range$$1 = this.sel.primary(), pos - if (start == null || start == "head") { pos = range$$1.head } - else if (start == "anchor") { pos = range$$1.anchor } - else if (start == "end" || start == "to" || start === false) { pos = range$$1.to() } - else { pos = range$$1.from() } + var range = this.sel.primary(), pos + if (start == null || start == "head") { pos = range.head } + else if (start == "anchor") { pos = range.anchor } + else if (start == "end" || start == "to" || start === false) { pos = range.to() } + else { pos = range.from() } return pos }, listSelections: function() { return this.sel.ranges }, @@ -5872,8 +6178,8 @@ Doc.prototype = createObj(BranchChunk.prototype, { var changes = [], sel = this.sel for (var i = 0; i < sel.ranges.length; i++) { - var range$$1 = sel.ranges[i] - changes[i] = {from: range$$1.from(), to: range$$1.to(), text: this$1.splitLines(code[i]), origin: origin} + var range = sel.ranges[i] + changes[i] = {from: range.from(), to: range.to(), text: this$1.splitLines(code[i]), origin: origin} } var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse) for (var i$1 = changes.length - 1; i$1 >= 0; i$1--) @@ -5931,7 +6237,6 @@ Doc.prototype = createObj(BranchChunk.prototype, { clearGutter: docMethodOp(function(gutterID) { var this$1 = this; - var i = this.first this.iter(function (line) { if (line.gutterMarkers && line.gutterMarkers[gutterID]) { changeLine(this$1, line, "gutter", function () { @@ -5940,7 +6245,6 @@ Doc.prototype = createObj(BranchChunk.prototype, { return true }) } - ++i }) }), @@ -6018,18 +6322,18 @@ Doc.prototype = createObj(BranchChunk.prototype, { }, findMarks: function(from, to, filter) { from = clipPos(this, from); to = clipPos(this, to) - var found = [], lineNo$$1 = from.line + var found = [], lineNo = from.line this.iter(from.line, to.line + 1, function (line) { var spans = line.markedSpans if (spans) { for (var i = 0; i < spans.length; i++) { var span = spans[i] - if (!(span.to != null && lineNo$$1 == from.line && from.ch >= span.to || - span.from == null && lineNo$$1 != from.line || - span.from != null && lineNo$$1 == to.line && span.from >= to.ch) && + if (!(span.to != null && lineNo == from.line && from.ch >= span.to || + span.from == null && lineNo != from.line || + span.from != null && lineNo == to.line && span.from >= to.ch) && (!filter || filter(span.marker))) { found.push(span.marker.parent || span.marker) } } } - ++lineNo$$1 + ++lineNo }) return found }, @@ -6044,14 +6348,14 @@ Doc.prototype = createObj(BranchChunk.prototype, { }, posFromIndex: function(off) { - var ch, lineNo$$1 = this.first, sepSize = this.lineSeparator().length + var ch, lineNo = this.first, sepSize = this.lineSeparator().length this.iter(function (line) { var sz = line.text.length + sepSize if (sz > off) { ch = off; return true } off -= sz - ++lineNo$$1 + ++lineNo }) - return clipPos(this, Pos(lineNo$$1, ch)) + return clipPos(this, Pos(lineNo, ch)) }, indexFromPos: function (coords) { coords = clipPos(this, coords) @@ -6066,7 +6370,7 @@ Doc.prototype = createObj(BranchChunk.prototype, { copy: function(copyHistory) { var doc = new Doc(getLines(this, this.first, this.first + this.size), - this.modeOption, this.first, this.lineSep) + this.modeOption, this.first, this.lineSep, this.direction) doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft doc.sel = this.sel doc.extend = false @@ -6082,7 +6386,7 @@ Doc.prototype = createObj(BranchChunk.prototype, { var from = this.first, to = this.first + this.size if (options.from != null && options.from > from) { from = options.from } if (options.to != null && options.to < to) { to = options.to } - var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep) + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep, this.direction) if (options.sharedHist) { copy.history = this.history ; }(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}) copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}] @@ -6092,7 +6396,7 @@ Doc.prototype = createObj(BranchChunk.prototype, { unlinkDoc: function(other) { var this$1 = this; - if (other instanceof CodeMirror$1) { other = other.doc } + if (other instanceof CodeMirror) { other = other.doc } if (this.linked) { for (var i = 0; i < this.linked.length; ++i) { var link = this$1.linked[i] if (link.doc != other) { continue } @@ -6119,7 +6423,15 @@ Doc.prototype = createObj(BranchChunk.prototype, { if (this.lineSep) { return str.split(this.lineSep) } return splitLinesAuto(str) }, - lineSeparator: function() { return this.lineSep || "\n" } + lineSeparator: function() { return this.lineSep || "\n" }, + + setDirection: docMethodOp(function (dir) { + if (dir != "rtl") { dir = "ltr" } + if (dir == this.direction) { return } + this.direction = dir + this.iter(function (line) { return line.order = null; }) + if (this.cm) { directionChanged(this.cm) } + }) }) // Public alias. @@ -6236,8 +6548,8 @@ function clearDragCursor(cm) { // garbage collected. function forEachCodeMirror(f) { - if (!document.body.getElementsByClassName) { return } - var byClass = document.body.getElementsByClassName("CodeMirror") + if (!document.getElementsByClassName) { return } + var byClass = document.getElementsByClassName("CodeMirror") for (var i = 0; i < byClass.length; i++) { var cm = byClass[i].CodeMirror if (cm) { f(cm) } @@ -6274,11 +6586,11 @@ function onResize(cm) { } var keyNames = { - 3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 3: "Pause", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", - 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete", + 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete", 145: "ScrollLock", 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" @@ -6369,7 +6681,7 @@ function normalizeKeyMap(keymap) { var keys = map(keyname.split(" "), normalizeKeyName) for (var i = 0; i < keys.length; i++) { - var val = void 0, name = void 0 + var val = (void 0), name = (void 0) if (i == keys.length - 1) { name = keys.join(" ") val = value @@ -6387,18 +6699,18 @@ function normalizeKeyMap(keymap) { return keymap } -function lookupKey(key, map$$1, handle, context) { - map$$1 = getKeyMap(map$$1) - var found = map$$1.call ? map$$1.call(key, context) : map$$1[key] +function lookupKey(key, map, handle, context) { + map = getKeyMap(map) + var found = map.call ? map.call(key, context) : map[key] if (found === false) { return "nothing" } if (found === "...") { return "multi" } if (found != null && handle(found)) { return "handled" } - if (map$$1.fallthrough) { - if (Object.prototype.toString.call(map$$1.fallthrough) != "[object Array]") - { return lookupKey(key, map$$1.fallthrough, handle, context) } - for (var i = 0; i < map$$1.fallthrough.length; i++) { - var result = lookupKey(key, map$$1.fallthrough[i], handle, context) + if (map.fallthrough) { + if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") + { return lookupKey(key, map.fallthrough, handle, context) } + for (var i = 0; i < map.fallthrough.length; i++) { + var result = lookupKey(key, map.fallthrough[i], handle, context) if (result) { return result } } } @@ -6411,11 +6723,8 @@ function isModifierKey(value) { return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod" } -// Look up the name of a key as indicated by an event object. -function keyName(event, noShift) { - if (presto && event.keyCode == 34 && event["char"]) { return false } - var base = keyNames[event.keyCode], name = base - if (name == null || event.altGraphKey) { return false } +function addModifierNames(name, event, noShift) { + var base = name if (event.altKey && base != "Alt") { name = "Alt-" + name } if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") { name = "Ctrl-" + name } if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") { name = "Cmd-" + name } @@ -6423,6 +6732,17 @@ function keyName(event, noShift) { return name } +// Look up the name of a key as indicated by an event object. +function keyName(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) { return false } + var name = keyNames[event.keyCode] + if (name == null || event.altGraphKey) { return false } + // Ctrl-ScrollLock has keyCode 3, same as Ctrl-Pause, + // so we'll use event.code when available (Chrome 48+, FF 38+, Safari 10.1+) + if (event.keyCode == 3 && event.code) { name = event.code } + return addModifierNames(name, event, noShift) +} + function getKeyMap(val) { return typeof val == "string" ? keyMap[val] : val } @@ -6452,6 +6772,112 @@ function deleteNearSelection(cm, compute) { }) } +function moveCharLogically(line, ch, dir) { + var target = skipExtendingChars(line.text, ch + dir, dir) + return target < 0 || target > line.text.length ? null : target +} + +function moveLogically(line, start, dir) { + var ch = moveCharLogically(line, start.ch, dir) + return ch == null ? null : new Pos(start.line, ch, dir < 0 ? "after" : "before") +} + +function endOfLine(visually, cm, lineObj, lineNo, dir) { + if (visually) { + var order = getOrder(lineObj, cm.doc.direction) + if (order) { + var part = dir < 0 ? lst(order) : order[0] + var moveInStorageOrder = (dir < 0) == (part.level == 1) + var sticky = moveInStorageOrder ? "after" : "before" + var ch + // With a wrapped rtl chunk (possibly spanning multiple bidi parts), + // it could be that the last bidi part is not on the last visual line, + // since visual lines contain content order-consecutive chunks. + // Thus, in rtl, we are looking for the first (content-order) character + // in the rtl chunk that is on the last line (that is, the same line + // as the last (content-order) character). + if (part.level > 0 || cm.doc.direction == "rtl") { + var prep = prepareMeasureForLine(cm, lineObj) + ch = dir < 0 ? lineObj.text.length - 1 : 0 + var targetTop = measureCharPrepared(cm, prep, ch).top + ch = findFirst(function (ch) { return measureCharPrepared(cm, prep, ch).top == targetTop; }, (dir < 0) == (part.level == 1) ? part.from : part.to - 1, ch) + if (sticky == "before") { ch = moveCharLogically(lineObj, ch, 1) } + } else { ch = dir < 0 ? part.to : part.from } + return new Pos(lineNo, ch, sticky) + } + } + return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? "before" : "after") +} + +function moveVisually(cm, line, start, dir) { + var bidi = getOrder(line, cm.doc.direction) + if (!bidi) { return moveLogically(line, start, dir) } + if (start.ch >= line.text.length) { + start.ch = line.text.length + start.sticky = "before" + } else if (start.ch <= 0) { + start.ch = 0 + start.sticky = "after" + } + var partPos = getBidiPartAt(bidi, start.ch, start.sticky), part = bidi[partPos] + if (cm.doc.direction == "ltr" && part.level % 2 == 0 && (dir > 0 ? part.to > start.ch : part.from < start.ch)) { + // Case 1: We move within an ltr part in an ltr editor. Even with wrapped lines, + // nothing interesting happens. + return moveLogically(line, start, dir) + } + + var mv = function (pos, dir) { return moveCharLogically(line, pos instanceof Pos ? pos.ch : pos, dir); } + var prep + var getWrappedLineExtent = function (ch) { + if (!cm.options.lineWrapping) { return {begin: 0, end: line.text.length} } + prep = prep || prepareMeasureForLine(cm, line) + return wrappedLineExtentChar(cm, line, prep, ch) + } + var wrappedLineExtent = getWrappedLineExtent(start.sticky == "before" ? mv(start, -1) : start.ch) + + if (cm.doc.direction == "rtl" || part.level == 1) { + var moveInStorageOrder = (part.level == 1) == (dir < 0) + var ch = mv(start, moveInStorageOrder ? 1 : -1) + if (ch != null && (!moveInStorageOrder ? ch >= part.from && ch >= wrappedLineExtent.begin : ch <= part.to && ch <= wrappedLineExtent.end)) { + // Case 2: We move within an rtl part or in an rtl editor on the same visual line + var sticky = moveInStorageOrder ? "before" : "after" + return new Pos(start.line, ch, sticky) + } + } + + // Case 3: Could not move within this bidi part in this visual line, so leave + // the current bidi part + + var searchInVisualLine = function (partPos, dir, wrappedLineExtent) { + var getRes = function (ch, moveInStorageOrder) { return moveInStorageOrder + ? new Pos(start.line, mv(ch, 1), "before") + : new Pos(start.line, ch, "after"); } + + for (; partPos >= 0 && partPos < bidi.length; partPos += dir) { + var part = bidi[partPos] + var moveInStorageOrder = (dir > 0) == (part.level != 1) + var ch = moveInStorageOrder ? wrappedLineExtent.begin : mv(wrappedLineExtent.end, -1) + if (part.from <= ch && ch < part.to) { return getRes(ch, moveInStorageOrder) } + ch = moveInStorageOrder ? part.from : mv(part.to, -1) + if (wrappedLineExtent.begin <= ch && ch < wrappedLineExtent.end) { return getRes(ch, moveInStorageOrder) } + } + } + + // Case 3a: Look for other bidi parts on the same visual line + var res = searchInVisualLine(partPos + dir, dir, wrappedLineExtent) + if (res) { return res } + + // Case 3b: Look for other bidi parts on the next visual line + var nextCh = dir > 0 ? wrappedLineExtent.end : mv(wrappedLineExtent.begin, -1) + if (nextCh != null && !(dir > 0 && nextCh == line.text.length)) { + res = searchInVisualLine(dir > 0 ? 0 : bidi.length - 1, dir, getWrappedLineExtent(nextCh)) + if (res) { return res } + } + + // Case 4: Nowhere to move + return null +} + // Commands are parameter-less actions that can be performed on an // editor, mostly used for keybindings. var commands = { @@ -6501,15 +6927,15 @@ var commands = { {origin: "+move", bias: -1} ); }, goLineRight: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.charCoords(range.head, "div").top + 5 + var top = cm.cursorCoords(range.head, "div").top + 5 return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div") }, sel_move); }, goLineLeft: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.charCoords(range.head, "div").top + 5 + var top = cm.cursorCoords(range.head, "div").top + 5 return cm.coordsChar({left: 0, top: top}, "div") }, sel_move); }, goLineLeftSmart: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.charCoords(range.head, "div").top + 5 + var top = cm.cursorCoords(range.head, "div").top + 5 var pos = cm.coordsChar({left: 0, top: top}, "div") if (pos.ch < cm.getLine(pos.line).search(/\S/)) { return lineStartSmart(cm, range.head) } return pos @@ -6599,28 +7025,22 @@ function lineStart(cm, lineN) { var line = getLine(cm.doc, lineN) var visual = visualLine(line) if (visual != line) { lineN = lineNo(visual) } - var order = getOrder(visual) - var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual) - return Pos(lineN, ch) + return endOfLine(true, cm, visual, lineN, 1) } function lineEnd(cm, lineN) { - var merged, line = getLine(cm.doc, lineN) - while (merged = collapsedSpanAtEnd(line)) { - line = merged.find(1, true).line - lineN = null - } - var order = getOrder(line) - var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line) - return Pos(lineN == null ? lineNo(line) : lineN, ch) + var line = getLine(cm.doc, lineN) + var visual = visualLineEnd(line) + if (visual != line) { lineN = lineNo(visual) } + return endOfLine(true, cm, line, lineN, -1) } function lineStartSmart(cm, pos) { var start = lineStart(cm, pos.line) var line = getLine(cm.doc, start.line) - var order = getOrder(line) + var order = getOrder(line, cm.doc.direction) if (!order || order[0].level == 0) { var firstNonWS = Math.max(0, line.text.search(/\S/)) var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch - return Pos(start.line, inWS ? 0 : firstNonWS) + return Pos(start.line, inWS ? 0 : firstNonWS, start.sticky) } return start } @@ -6655,19 +7075,30 @@ function lookupKeyForEditor(cm, name, handle) { || lookupKey(name, cm.options.keyMap, handle, cm) } +// Note that, despite the name, this function is also used to check +// for bound mouse clicks. + var stopSeq = new Delayed + function dispatchKey(cm, name, e, handle) { var seq = cm.state.keySeq if (seq) { if (isModifierKey(name)) { return "handled" } - stopSeq.set(50, function () { - if (cm.state.keySeq == seq) { - cm.state.keySeq = null - cm.display.input.reset() - } - }) - name = seq + " " + name + if (/\'$/.test(name)) + { cm.state.keySeq = null } + else + { stopSeq.set(50, function () { + if (cm.state.keySeq == seq) { + cm.state.keySeq = null + cm.display.input.reset() + } + }) } + if (dispatchKeyInner(cm, seq + " " + name, e, handle)) { return true } } + return dispatchKeyInner(cm, name, e, handle) +} + +function dispatchKeyInner(cm, name, e, handle) { var result = lookupKeyForEditor(cm, name, handle) if (result == "multi") @@ -6680,10 +7111,6 @@ function dispatchKey(cm, name, e, handle) { restartBlink(cm) } - if (seq && !result && /\'$/.test(name)) { - e_preventDefault(e) - return true - } return !!result } @@ -6753,7 +7180,7 @@ function onKeyUp(e) { signalDOMEvent(this, e) } -function onKeyPress$1(e) { +function onKeyPress(e) { var cm = this if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) { return } var keyCode = e.keyCode, charCode = e.charCode @@ -6766,6 +7193,37 @@ function onKeyPress$1(e) { cm.display.input.onKeyPress(e) } +var DOUBLECLICK_DELAY = 400 + +var PastClick = function(time, pos, button) { + this.time = time + this.pos = pos + this.button = button +}; + +PastClick.prototype.compare = function (time, pos, button) { + return this.time + DOUBLECLICK_DELAY > time && + cmp(pos, this.pos) == 0 && button == this.button +}; + +var lastClick; +var lastDoubleClick; +function clickRepeat(pos, button) { + var now = +new Date + if (lastDoubleClick && lastDoubleClick.compare(now, pos, button)) { + lastClick = lastDoubleClick = null + return "triple" + } else if (lastClick && lastClick.compare(now, pos, button)) { + lastDoubleClick = new PastClick(now, pos, button) + lastClick = null + return "double" + } else { + lastClick = new PastClick(now, pos, button) + lastDoubleClick = null + return "single" + } +} + // A mouse down can be a single click, double click, triple click, // start of selection drag, start of text drag, new cursor // (ctrl-click), rectangle drag (alt-drag), or xwin @@ -6787,96 +7245,132 @@ function onMouseDown(e) { return } if (clickInGutter(cm, e)) { return } - var start = posFromMouse(cm, e) + var pos = posFromMouse(cm, e), button = e_button(e), repeat = pos ? clickRepeat(pos, button) : "single" window.focus() - switch (e_button(e)) { - case 1: - // #3261: make sure, that we're not starting a second selection - if (cm.state.selectingText) - { cm.state.selectingText(e) } - else if (start) - { leftButtonDown(cm, e, start) } - else if (e_target(e) == display.scroller) - { e_preventDefault(e) } - break - case 2: - if (webkit) { cm.state.lastMiddleDown = +new Date } - if (start) { extendSelection(cm.doc, start) } + // #3261: make sure, that we're not starting a second selection + if (button == 1 && cm.state.selectingText) + { cm.state.selectingText(e) } + + if (pos && handleMappedButton(cm, button, pos, repeat, e)) { return } + + if (button == 1) { + if (pos) { leftButtonDown(cm, pos, repeat, e) } + else if (e_target(e) == display.scroller) { e_preventDefault(e) } + } else if (button == 2) { + if (pos) { extendSelection(cm.doc, pos) } setTimeout(function () { return display.input.focus(); }, 20) - e_preventDefault(e) - break - case 3: - if (captureRightClick) { onContextMenu$1(cm, e) } + } else if (button == 3) { + if (captureRightClick) { onContextMenu(cm, e) } else { delayBlurEvent(cm) } - break } } -var lastClick; -var lastDoubleClick -function leftButtonDown(cm, e, start) { +function handleMappedButton(cm, button, pos, repeat, event) { + var name = "Click" + if (repeat == "double") { name = "Double" + name } + else if (repeat == "triple") { name = "Triple" + name } + name = (button == 1 ? "Left" : button == 2 ? "Middle" : "Right") + name + + return dispatchKey(cm, addModifierNames(name, event), event, function (bound) { + if (typeof bound == "string") { bound = commands[bound] } + if (!bound) { return false } + var done = false + try { + if (cm.isReadOnly()) { cm.state.suppressEdits = true } + done = bound(cm, pos) != Pass + } finally { + cm.state.suppressEdits = false + } + return done + }) +} + +function configureMouse(cm, repeat, event) { + var option = cm.getOption("configureMouse") + var value = option ? option(cm, repeat, event) : {} + if (value.unit == null) { + var rect = chromeOS ? event.shiftKey && event.metaKey : event.altKey + value.unit = rect ? "rectangle" : repeat == "single" ? "char" : repeat == "double" ? "word" : "line" + } + if (value.extend == null || cm.doc.extend) { value.extend = cm.doc.extend || event.shiftKey } + if (value.addNew == null) { value.addNew = mac ? event.metaKey : event.ctrlKey } + if (value.moveOnDrag == null) { value.moveOnDrag = !(mac ? event.altKey : event.ctrlKey) } + return value +} + +function leftButtonDown(cm, pos, repeat, event) { if (ie) { setTimeout(bind(ensureFocus, cm), 0) } else { cm.curOp.focus = activeElt() } - var now = +new Date, type - if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { - type = "triple" - } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { - type = "double" - lastDoubleClick = {time: now, pos: start} - } else { - type = "single" - lastClick = {time: now, pos: start} - } + var behavior = configureMouse(cm, repeat, event) - var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained + var sel = cm.doc.sel, contained if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() && - type == "single" && (contained = sel.contains(start)) > -1 && - (cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) && - (cmp(contained.to(), start) > 0 || start.xRel < 0)) - { leftButtonStartDrag(cm, e, start, modifier) } + repeat == "single" && (contained = sel.contains(pos)) > -1 && + (cmp((contained = sel.ranges[contained]).from(), pos) < 0 || pos.xRel > 0) && + (cmp(contained.to(), pos) > 0 || pos.xRel < 0)) + { leftButtonStartDrag(cm, event, pos, behavior) } else - { leftButtonSelect(cm, e, start, type, modifier) } + { leftButtonSelect(cm, event, pos, behavior) } } // Start a text drag. When it ends, see if any dragging actually // happen, and treat as a click if it didn't. -function leftButtonStartDrag(cm, e, start, modifier) { - var display = cm.display, startTime = +new Date - var dragEnd = operation(cm, function (e2) { +function leftButtonStartDrag(cm, event, pos, behavior) { + var display = cm.display, moved = false + var dragEnd = operation(cm, function (e) { if (webkit) { display.scroller.draggable = false } cm.state.draggingText = false - off(document, "mouseup", dragEnd) + off(display.wrapper.ownerDocument, "mouseup", dragEnd) + off(display.wrapper.ownerDocument, "mousemove", mouseMove) + off(display.scroller, "dragstart", dragStart) off(display.scroller, "drop", dragEnd) - if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { - e_preventDefault(e2) - if (!modifier && +new Date - 200 < startTime) - { extendSelection(cm.doc, start) } + if (!moved) { + e_preventDefault(e) + if (!behavior.addNew) + { extendSelection(cm.doc, pos, null, null, behavior.extend) } // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) if (webkit || ie && ie_version == 9) - { setTimeout(function () {document.body.focus(); display.input.focus()}, 20) } + { setTimeout(function () {display.wrapper.ownerDocument.body.focus(); display.input.focus()}, 20) } else { display.input.focus() } } }) + var mouseMove = function(e2) { + moved = moved || Math.abs(event.clientX - e2.clientX) + Math.abs(event.clientY - e2.clientY) >= 10 + } + var dragStart = function () { return moved = true; } // Let the drag handler handle this. if (webkit) { display.scroller.draggable = true } cm.state.draggingText = dragEnd - dragEnd.copy = mac ? e.altKey : e.ctrlKey + dragEnd.copy = !behavior.moveOnDrag // IE's approach to draggable if (display.scroller.dragDrop) { display.scroller.dragDrop() } - on(document, "mouseup", dragEnd) + on(display.wrapper.ownerDocument, "mouseup", dragEnd) + on(display.wrapper.ownerDocument, "mousemove", mouseMove) + on(display.scroller, "dragstart", dragStart) on(display.scroller, "drop", dragEnd) + + delayBlurEvent(cm) + setTimeout(function () { return display.input.focus(); }, 20) +} + +function rangeForUnit(cm, pos, unit) { + if (unit == "char") { return new Range(pos, pos) } + if (unit == "word") { return cm.findWordAt(pos) } + if (unit == "line") { return new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) } + var result = unit(cm, pos) + return new Range(result.from, result.to) } // Normal selection, as opposed to text dragging. -function leftButtonSelect(cm, e, start, type, addNew) { +function leftButtonSelect(cm, event, start, behavior) { var display = cm.display, doc = cm.doc - e_preventDefault(e) + e_preventDefault(event) var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges - if (addNew && !e.shiftKey) { + if (behavior.addNew && !behavior.extend) { ourIndex = doc.sel.contains(start) if (ourIndex > -1) { ourRange = ranges[ourIndex] } @@ -6887,28 +7381,19 @@ function leftButtonSelect(cm, e, start, type, addNew) { ourIndex = doc.sel.primIndex } - if (chromeOS ? e.shiftKey && e.metaKey : e.altKey) { - type = "rect" - if (!addNew) { ourRange = new Range(start, start) } - start = posFromMouse(cm, e, true, true) + if (behavior.unit == "rectangle") { + if (!behavior.addNew) { ourRange = new Range(start, start) } + start = posFromMouse(cm, event, true, true) ourIndex = -1 - } else if (type == "double") { - var word = cm.findWordAt(start) - if (cm.display.shift || doc.extend) - { ourRange = extendRange(doc, ourRange, word.anchor, word.head) } - else - { ourRange = word } - } else if (type == "triple") { - var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))) - if (cm.display.shift || doc.extend) - { ourRange = extendRange(doc, ourRange, line.anchor, line.head) } - else - { ourRange = line } } else { - ourRange = extendRange(doc, ourRange, start) + var range = rangeForUnit(cm, start, behavior.unit) + if (behavior.extend) + { ourRange = extendRange(ourRange, range.anchor, range.head, behavior.extend) } + else + { ourRange = range } } - if (!addNew) { + if (!behavior.addNew) { ourIndex = 0 setSelection(doc, new Selection([ourRange], 0), sel_mouse) startSel = doc.sel @@ -6916,7 +7401,7 @@ function leftButtonSelect(cm, e, start, type, addNew) { ourIndex = ranges.length setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), {scroll: false, origin: "*mouse"}) - } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) { + } else if (ranges.length > 1 && ranges[ourIndex].empty() && behavior.unit == "char" && !behavior.extend) { setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), {scroll: false, origin: "*mouse"}) startSel = doc.sel @@ -6929,7 +7414,7 @@ function leftButtonSelect(cm, e, start, type, addNew) { if (cmp(lastPos, pos) == 0) { return } lastPos = pos - if (type == "rect") { + if (behavior.unit == "rectangle") { var ranges = [], tabSize = cm.options.tabSize var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize) var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize) @@ -6948,23 +7433,17 @@ function leftButtonSelect(cm, e, start, type, addNew) { cm.scrollIntoView(pos) } else { var oldRange = ourRange - var anchor = oldRange.anchor, head = pos - if (type != "single") { - var range$$1 - if (type == "double") - { range$$1 = cm.findWordAt(pos) } - else - { range$$1 = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))) } - if (cmp(range$$1.anchor, anchor) > 0) { - head = range$$1.head - anchor = minPos(oldRange.from(), range$$1.anchor) - } else { - head = range$$1.anchor - anchor = maxPos(oldRange.to(), range$$1.head) - } + var range = rangeForUnit(cm, pos, behavior.unit) + var anchor = oldRange.anchor, head + if (cmp(range.anchor, anchor) > 0) { + head = range.head + anchor = minPos(oldRange.from(), range.anchor) + } else { + head = range.anchor + anchor = maxPos(oldRange.to(), range.head) } var ranges$1 = startSel.ranges.slice(0) - ranges$1[ourIndex] = new Range(clipPos(doc, anchor), head) + ranges$1[ourIndex] = bidiSimplify(cm, new Range(clipPos(doc, anchor), head)) setSelection(doc, normalizeSelection(ranges$1, ourIndex), sel_mouse) } } @@ -6978,7 +7457,7 @@ function leftButtonSelect(cm, e, start, type, addNew) { function extend(e) { var curCount = ++counter - var cur = posFromMouse(cm, e, true, type == "rect") + var cur = posFromMouse(cm, e, true, behavior.unit == "rectangle") if (!cur) { return } if (cmp(cur, lastPos) != 0) { cm.curOp.focus = activeElt() @@ -7001,19 +7480,53 @@ function leftButtonSelect(cm, e, start, type, addNew) { counter = Infinity e_preventDefault(e) display.input.focus() - off(document, "mousemove", move) - off(document, "mouseup", up) + off(display.wrapper.ownerDocument, "mousemove", move) + off(display.wrapper.ownerDocument, "mouseup", up) doc.history.lastSelOrigin = null } var move = operation(cm, function (e) { - if (!e_button(e)) { done(e) } + if (e.buttons === 0 || !e_button(e)) { done(e) } else { extend(e) } }) var up = operation(cm, done) cm.state.selectingText = up - on(document, "mousemove", move) - on(document, "mouseup", up) + on(display.wrapper.ownerDocument, "mousemove", move) + on(display.wrapper.ownerDocument, "mouseup", up) +} + +// Used when mouse-selecting to adjust the anchor to the proper side +// of a bidi jump depending on the visual position of the head. +function bidiSimplify(cm, range) { + var anchor = range.anchor; + var head = range.head; + var anchorLine = getLine(cm.doc, anchor.line) + if (cmp(anchor, head) == 0 && anchor.sticky == head.sticky) { return range } + var order = getOrder(anchorLine) + if (!order) { return range } + var index = getBidiPartAt(order, anchor.ch, anchor.sticky), part = order[index] + if (part.from != anchor.ch && part.to != anchor.ch) { return range } + var boundary = index + ((part.from == anchor.ch) == (part.level != 1) ? 0 : 1) + if (boundary == 0 || boundary == order.length) { return range } + + // Compute the relative visual position of the head compared to the + // anchor (<0 is to the left, >0 to the right) + var leftSide + if (head.line != anchor.line) { + leftSide = (head.line - anchor.line) * (cm.doc.direction == "ltr" ? 1 : -1) > 0 + } else { + var headIndex = getBidiPartAt(order, head.ch, head.sticky) + var dir = headIndex - index || (head.ch - anchor.ch) * (part.level == 1 ? -1 : 1) + if (headIndex == boundary - 1 || headIndex == boundary) + { leftSide = dir < 0 } + else + { leftSide = dir > 0 } + } + + var usePart = order[boundary + (leftSide ? -1 : 0)] + var from = leftSide == (usePart.level == 1) + var ch = from ? usePart.from : usePart.to, sticky = from ? "after" : "before" + return anchor.ch == ch && anchor.sticky == sticky ? range : new Range(new Pos(anchor.line, ch, sticky), head) } @@ -7021,8 +7534,13 @@ function leftButtonSelect(cm, e, start, type, addNew) { // handlers for the corresponding event. function gutterEvent(cm, e, type, prevent) { var mX, mY - try { mX = e.clientX; mY = e.clientY } - catch(e) { return false } + if (e.touches) { + mX = e.touches[0].clientX + mY = e.touches[0].clientY + } else { + try { mX = e.clientX; mY = e.clientY } + catch(e) { return false } + } if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) { return false } if (prevent) { e_preventDefault(e) } @@ -7052,7 +7570,7 @@ function clickInGutter(cm, e) { // To make the context menu work, we need to briefly unhide the // textarea (making it as unobtrusive as possible) to let the // right-click take effect on it. -function onContextMenu$1(cm, e) { +function onContextMenu(cm, e) { if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) { return } if (signalDOMEvent(cm, e, "contextmenu")) { return } cm.display.input.onContextMenu(e) @@ -7104,6 +7622,7 @@ function defineOptions(CodeMirror) { clearCaches(cm) regChange(cm) }, true) + option("lineSeparator", null, function (cm, val) { cm.doc.lineSep = val if (!val) { return } @@ -7120,7 +7639,7 @@ function defineOptions(CodeMirror) { for (var i = newBreaks.length - 1; i >= 0; i--) { replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)) } }) - option("specialChars", /[\u0000-\u001f\u007f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/g, function (cm, val, old) { + option("specialChars", /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/g, function (cm, val, old) { cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g") if (old != Init) { cm.refresh() } }) @@ -7144,6 +7663,7 @@ function defineOptions(CodeMirror) { if (next.attach) { next.attach(cm, prev || null) } }) option("extraKeys", null) + option("configureMouse", null) option("lineWrapping", false, wrappingChanged, true) option("gutters", [], function (cm) { @@ -7171,14 +7691,12 @@ function defineOptions(CodeMirror) { option("resetSelectionOnContextMenu", true) option("lineWiseCopyCut", true) + option("pasteLinesPerSelection", true) option("readOnly", false, function (cm, val) { if (val == "nocursor") { onBlur(cm) cm.display.input.blur() - cm.display.disabled = true - } else { - cm.display.disabled = false } cm.display.input.readOnlyChanged(val) }) @@ -7205,6 +7723,7 @@ function defineOptions(CodeMirror) { option("tabindex", null, function (cm, val) { return cm.display.input.getField().tabIndex = val || ""; }) option("autofocus", null) + option("direction", "ltr", function (cm, val) { return cm.doc.setDirection(val); }, true) } function guttersChanged(cm) { @@ -7244,10 +7763,10 @@ function wrappingChanged(cm) { // A CodeMirror instance represents an editor. This is the object // that user code is usually dealing with. -function CodeMirror$1(place, options) { +function CodeMirror(place, options) { var this$1 = this; - if (!(this instanceof CodeMirror$1)) { return new CodeMirror$1(place, options) } + if (!(this instanceof CodeMirror)) { return new CodeMirror(place, options) } this.options = options = options ? copyObj(options) : {} // Determine effective options based on given values and defaults. @@ -7255,10 +7774,10 @@ function CodeMirror$1(place, options) { setGuttersForLineNumbers(options) var doc = options.value - if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator) } + if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction) } this.doc = doc - var input = new CodeMirror$1.inputStyles[options.inputStyle](this) + var input = new CodeMirror.inputStyles[options.inputStyle](this) var display = this.display = new Display(place, doc, input) display.wrapper.CodeMirror = this updateGutters(this) @@ -7315,9 +7834,9 @@ function CodeMirror$1(place, options) { } // The default configuration options. -CodeMirror$1.defaults = defaults +CodeMirror.defaults = defaults // Functions to run when options are changed. -CodeMirror$1.optionHandlers = optionHandlers +CodeMirror.optionHandlers = optionHandlers // Attach the necessary event handlers when initializing the editor function registerEventHandlers(cm) { @@ -7338,7 +7857,7 @@ function registerEventHandlers(cm) { // Some browsers fire contextmenu *after* opening the menu, at // which point we can't mess with it anymore. Context menu is // handled in onMouseDown for these browsers. - if (!captureRightClick) { on(d.scroller, "contextmenu", function (e) { return onContextMenu$1(cm, e); }) } + if (!captureRightClick) { on(d.scroller, "contextmenu", function (e) { return onContextMenu(cm, e); }) } // Used to suppress mouse event handling when a touch happens var touchFinished, prevTouch = {end: 0} @@ -7360,7 +7879,7 @@ function registerEventHandlers(cm) { return dx * dx + dy * dy > 20 * 20 } on(d.scroller, "touchstart", function (e) { - if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e)) { + if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e) && !clickInGutter(cm, e)) { d.input.ensurePolled() clearTimeout(touchFinished) var now = +new Date @@ -7398,7 +7917,7 @@ function registerEventHandlers(cm) { // area, ensure viewport is updated when scrolling. on(d.scroller, "scroll", function () { if (d.scroller.clientHeight) { - setScrollTop(cm, d.scroller.scrollTop) + updateScrollTop(cm, d.scroller.scrollTop) setScrollLeft(cm, d.scroller.scrollLeft, true) signal(cm, "scroll", cm) } @@ -7422,13 +7941,13 @@ function registerEventHandlers(cm) { var inp = d.input.getField() on(inp, "keyup", function (e) { return onKeyUp.call(cm, e); }) on(inp, "keydown", operation(cm, onKeyDown)) - on(inp, "keypress", operation(cm, onKeyPress$1)) + on(inp, "keypress", operation(cm, onKeyPress)) on(inp, "focus", function (e) { return onFocus(cm, e); }) on(inp, "blur", function (e) { return onBlur(cm, e); }) } var initHooks = [] -CodeMirror$1.defineInitHook = function (f) { return initHooks.push(f); } +CodeMirror.defineInitHook = function (f) { return initHooks.push(f); } // Indent the given line. The how parameter can be "smart", // "add"/null, "subtract", or "prev". When aggressive is false @@ -7442,7 +7961,7 @@ function indentLine(cm, n, how, aggressive) { // Fall back to "prev" when the mode doesn't have an indentation // method. if (!doc.mode.indent) { how = "prev" } - else { state = getStateBefore(cm, n) } + else { state = getContextBefore(cm, n).state } } var tabSize = cm.options.tabSize @@ -7510,7 +8029,7 @@ function applyTextInput(cm, inserted, deleted, sel, origin) { var paste = cm.state.pasteIncoming || origin == "paste" var textLines = splitLinesAuto(inserted), multiPaste = null - // When pasing N lines into N selections, insert one line per selection + // When pasting N lines into N selections, insert one line per selection if (paste && sel.ranges.length > 1) { if (lastCopied && lastCopied.text.join("\n") == inserted) { if (sel.ranges.length % lastCopied.text.length == 0) { @@ -7518,7 +8037,7 @@ function applyTextInput(cm, inserted, deleted, sel, origin) { for (var i = 0; i < lastCopied.text.length; i++) { multiPaste.push(doc.splitLines(lastCopied.text[i])) } } - } else if (textLines.length == sel.ranges.length) { + } else if (textLines.length == sel.ranges.length && cm.options.pasteLinesPerSelection) { multiPaste = map(textLines, function (l) { return [l]; }) } } @@ -7526,9 +8045,9 @@ function applyTextInput(cm, inserted, deleted, sel, origin) { var updateInput // Normal behavior is to insert the new text into every selection for (var i$1 = sel.ranges.length - 1; i$1 >= 0; i$1--) { - var range$$1 = sel.ranges[i$1] - var from = range$$1.from(), to = range$$1.to() - if (range$$1.empty()) { + var range = sel.ranges[i$1] + var from = range.from(), to = range.to() + if (range.empty()) { if (deleted && deleted > 0) // Handle deletion { from = Pos(from.line, from.ch - deleted) } else if (cm.state.overwrite && !paste) // Handle overwrite @@ -7567,21 +8086,21 @@ function triggerElectric(cm, inserted) { var sel = cm.doc.sel for (var i = sel.ranges.length - 1; i >= 0; i--) { - var range$$1 = sel.ranges[i] - if (range$$1.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range$$1.head.line)) { continue } - var mode = cm.getModeAt(range$$1.head) + var range = sel.ranges[i] + if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) { continue } + var mode = cm.getModeAt(range.head) var indented = false if (mode.electricChars) { for (var j = 0; j < mode.electricChars.length; j++) { if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { - indented = indentLine(cm, range$$1.head.line, "smart") + indented = indentLine(cm, range.head.line, "smart") break } } } else if (mode.electricInput) { - if (mode.electricInput.test(getLine(cm.doc, range$$1.head.line).text.slice(0, range$$1.head.ch))) - { indented = indentLine(cm, range$$1.head.line, "smart") } + if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) + { indented = indentLine(cm, range.head.line, "smart") } } - if (indented) { signalLater(cm, "electricInput", cm, range$$1.head.line) } + if (indented) { signalLater(cm, "electricInput", cm, range.head.line) } } } @@ -7625,7 +8144,7 @@ function hiddenTextarea() { // CodeMirror.prototype, for backwards compatibility and // convenience. -var addEditorMethods = function(CodeMirror) { +function addEditorMethods(CodeMirror) { var optionHandlers = CodeMirror.optionHandlers var helpers = CodeMirror.helpers = {} @@ -7646,13 +8165,13 @@ var addEditorMethods = function(CodeMirror) { getOption: function(option) {return this.options[option]}, getDoc: function() {return this.doc}, - addKeyMap: function(map$$1, bottom) { - this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map$$1)) + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)) }, - removeKeyMap: function(map$$1) { + removeKeyMap: function(map) { var maps = this.state.keyMaps for (var i = 0; i < maps.length; ++i) - { if (maps[i] == map$$1 || maps[i].name == map$$1) { + { if (maps[i] == map || maps[i].name == map) { maps.splice(i, 1) return true } } @@ -7695,9 +8214,9 @@ var addEditorMethods = function(CodeMirror) { var ranges = this.doc.sel.ranges, end = -1 for (var i = 0; i < ranges.length; i++) { - var range$$1 = ranges[i] - if (!range$$1.empty()) { - var from = range$$1.from(), to = range$$1.to() + var range = ranges[i] + if (!range.empty()) { + var from = range.from(), to = range.to() var start = Math.max(end, from.line) end = Math.min(this$1.lastLine(), to.line - (to.ch ? 0 : 1)) + 1 for (var j = start; j < end; ++j) @@ -7705,9 +8224,9 @@ var addEditorMethods = function(CodeMirror) { var newRanges = this$1.doc.sel.ranges if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) { replaceOneSelection(this$1.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll) } - } else if (range$$1.head.line > end) { - indentLine(this$1, range$$1.head.line, how, true) - end = range$$1.head.line + } else if (range.head.line > end) { + indentLine(this$1, range.head.line, how, true) + end = range.head.line if (i == this$1.doc.sel.primIndex) { ensureCursorVisible(this$1) } } } @@ -7778,14 +8297,14 @@ var addEditorMethods = function(CodeMirror) { getStateAfter: function(line, precise) { var doc = this.doc line = clipLine(doc, line == null ? doc.first + doc.size - 1: line) - return getStateBefore(this, line + 1, precise) + return getContextBefore(this, line + 1, precise).state }, cursorCoords: function(start, mode) { - var pos, range$$1 = this.doc.sel.primary() - if (start == null) { pos = range$$1.head } + var pos, range = this.doc.sel.primary() + if (start == null) { pos = range.head } else if (typeof start == "object") { pos = clipPos(this.doc, start) } - else { pos = start ? range$$1.from() : range$$1.to() } + else { pos = start ? range.from() : range.to() } return cursorCoords(this, pos, mode || "page") }, @@ -7812,7 +8331,7 @@ var addEditorMethods = function(CodeMirror) { } else { lineObj = line } - return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page", includeWidgets).top + + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page", includeWidgets || end).top + (end ? this.doc.height - heightAtLine(lineObj) : 0) }, @@ -7853,12 +8372,13 @@ var addEditorMethods = function(CodeMirror) { node.style.left = left + "px" } if (scroll) - { scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight) } + { scrollIntoView(this, {left: left, top: top, right: left + node.offsetWidth, bottom: top + node.offsetHeight}) } }, triggerOnKeyDown: methodOp(onKeyDown), - triggerOnKeyPress: methodOp(onKeyPress$1), + triggerOnKeyPress: methodOp(onKeyPress), triggerOnKeyUp: onKeyUp, + triggerOnMouseDown: methodOp(onMouseDown), execCommand: function(cmd) { if (commands.hasOwnProperty(cmd)) @@ -7883,11 +8403,11 @@ var addEditorMethods = function(CodeMirror) { moveH: methodOp(function(dir, unit) { var this$1 = this; - this.extendSelectionsBy(function (range$$1) { - if (this$1.display.shift || this$1.doc.extend || range$$1.empty()) - { return findPosH(this$1.doc, range$$1.head, dir, unit, this$1.options.rtlMoveVisually) } + this.extendSelectionsBy(function (range) { + if (this$1.display.shift || this$1.doc.extend || range.empty()) + { return findPosH(this$1.doc, range.head, dir, unit, this$1.options.rtlMoveVisually) } else - { return dir < 0 ? range$$1.from() : range$$1.to() } + { return dir < 0 ? range.from() : range.to() } }, sel_move) }), @@ -7896,9 +8416,9 @@ var addEditorMethods = function(CodeMirror) { if (sel.somethingSelected()) { doc.replaceSelection("", null, "+delete") } else - { deleteNearSelection(this, function (range$$1) { - var other = findPosH(doc, range$$1.head, dir, unit, false) - return dir < 0 ? {from: other, to: range$$1.head} : {from: range$$1.head, to: other} + { deleteNearSelection(this, function (range) { + var other = findPosH(doc, range.head, dir, unit, false) + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other} }) } }), @@ -7923,15 +8443,15 @@ var addEditorMethods = function(CodeMirror) { var doc = this.doc, goals = [] var collapse = !this.display.shift && !doc.extend && doc.sel.somethingSelected() - doc.extendSelectionsBy(function (range$$1) { + doc.extendSelectionsBy(function (range) { if (collapse) - { return dir < 0 ? range$$1.from() : range$$1.to() } - var headPos = cursorCoords(this$1, range$$1.head, "div") - if (range$$1.goalColumn != null) { headPos.left = range$$1.goalColumn } + { return dir < 0 ? range.from() : range.to() } + var headPos = cursorCoords(this$1, range.head, "div") + if (range.goalColumn != null) { headPos.left = range.goalColumn } goals.push(headPos.left) var pos = findPosV(this$1, headPos, dir, unit) - if (unit == "page" && range$$1 == doc.sel.primary()) - { addToScrollPos(this$1, null, charCoords(this$1, pos, "div").top - headPos.top) } + if (unit == "page" && range == doc.sel.primary()) + { addToScrollTop(this$1, charCoords(this$1, pos, "div").top - headPos.top) } return pos }, sel_move) if (goals.length) { for (var i = 0; i < doc.sel.ranges.length; i++) @@ -7944,7 +8464,7 @@ var addEditorMethods = function(CodeMirror) { var start = pos.ch, end = pos.ch if (line) { var helper = this.getHelper(pos, "wordChars") - if ((pos.xRel < 0 || end == line.length) && start) { --start; } else { ++end } + if ((pos.sticky == "before" || end == line.length) && start) { --start; } else { ++end } var startChar = line.charAt(start) var check = isWordChar(startChar, helper) ? function (ch) { return isWordChar(ch, helper); } @@ -7968,11 +8488,7 @@ var addEditorMethods = function(CodeMirror) { hasFocus: function() { return this.display.input.getField() == activeElt() }, isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit) }, - scrollTo: methodOp(function(x, y) { - if (x != null || y != null) { resolveScrollToPos(this) } - if (x != null) { this.curOp.scrollLeft = x } - if (y != null) { this.curOp.scrollTop = y } - }), + scrollTo: methodOp(function (x, y) { scrollToCoords(this, x, y) }), getScrollInfo: function() { var scroller = this.display.scroller return {left: scroller.scrollLeft, top: scroller.scrollTop, @@ -7981,27 +8497,22 @@ var addEditorMethods = function(CodeMirror) { clientHeight: displayHeight(this), clientWidth: displayWidth(this)} }, - scrollIntoView: methodOp(function(range$$1, margin) { - if (range$$1 == null) { - range$$1 = {from: this.doc.sel.primary().head, to: null} + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null} if (margin == null) { margin = this.options.cursorScrollMargin } - } else if (typeof range$$1 == "number") { - range$$1 = {from: Pos(range$$1, 0), to: null} - } else if (range$$1.from == null) { - range$$1 = {from: range$$1, 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$$1.to) { range$$1.to = range$$1.from } - range$$1.margin = margin || 0 + if (!range.to) { range.to = range.from } + range.margin = margin || 0 - if (range$$1.from.line != null) { - resolveScrollToPos(this) - this.curOp.scrollToPos = range$$1 + if (range.from.line != null) { + scrollToRange(this, range) } else { - var sPos = calculateScrollPos(this, Math.min(range$$1.from.left, range$$1.to.left), - Math.min(range$$1.from.top, range$$1.to.top) - range$$1.margin, - Math.max(range$$1.from.right, range$$1.to.right), - Math.max(range$$1.from.bottom, range$$1.to.bottom) + range$$1.margin) - this.scrollTo(sPos.scrollLeft, sPos.scrollTop) + scrollToCoordsRange(this, range.from, range.to, range.margin) } }), @@ -8012,24 +8523,26 @@ var addEditorMethods = function(CodeMirror) { if (width != null) { this.display.wrapper.style.width = interpret(width) } if (height != null) { this.display.wrapper.style.height = interpret(height) } if (this.options.lineWrapping) { clearLineMeasurementCache(this) } - var lineNo$$1 = this.display.viewFrom - this.doc.iter(lineNo$$1, this.display.viewTo, function (line) { + var lineNo = this.display.viewFrom + this.doc.iter(lineNo, this.display.viewTo, function (line) { if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) - { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo$$1, "widget"); break } } } - ++lineNo$$1 + { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo, "widget"); break } } } + ++lineNo }) this.curOp.forceUpdate = true signal(this, "refresh", this) }), operation: function(f){return runInOp(this, f)}, + startOperation: function(){return startOperation(this)}, + endOperation: function(){return endOperation(this)}, refresh: methodOp(function() { var oldHeight = this.display.cachedTextHeight regChange(this) this.curOp.forceUpdate = true clearCaches(this) - this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop) + scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop) updateGutterSpace(this) if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) { estimateLineHeights(this) } @@ -8042,7 +8555,7 @@ var addEditorMethods = function(CodeMirror) { attachDoc(this, doc) clearCaches(this) this.display.input.reset() - this.scrollTo(doc.scrollLeft, doc.scrollTop) + scrollToCoords(this, doc.scrollLeft, doc.scrollTop) this.curOp.forceScroll = true signalLater(this, "swapDoc", this, old) return old @@ -8075,22 +8588,30 @@ var addEditorMethods = function(CodeMirror) { // position. The resulting position will have a hitSide=true // property if it reached the end of the document. function findPosH(doc, pos, dir, unit, visually) { - var line = pos.line, ch = pos.ch, origDir = dir - var lineObj = getLine(doc, line) + var oldPos = pos + var origDir = dir + var lineObj = getLine(doc, pos.line) function findNextLine() { - var l = line + dir + var l = pos.line + dir if (l < doc.first || l >= doc.first + doc.size) { return false } - line = l + pos = new Pos(l, pos.ch, pos.sticky) return lineObj = getLine(doc, l) } function moveOnce(boundToLine) { - var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true) + var next + if (visually) { + next = moveVisually(doc.cm, lineObj, pos, dir) + } else { + next = moveLogically(lineObj, pos, dir) + } if (next == null) { - if (!boundToLine && findNextLine()) { - if (visually) { ch = (dir < 0 ? lineRight : lineLeft)(lineObj) } - else { ch = dir < 0 ? lineObj.text.length : 0 } - } else { return false } - } else { ch = next } + if (!boundToLine && findNextLine()) + { pos = endOfLine(visually, doc.cm, lineObj, pos.line, dir) } + else + { return false } + } else { + pos = next + } return true } @@ -8103,14 +8624,14 @@ function findPosH(doc, pos, dir, unit, visually) { var helper = doc.cm && doc.cm.getHelper(pos, "wordChars") for (var first = true;; first = false) { if (dir < 0 && !moveOnce(!first)) { break } - var cur = lineObj.text.charAt(ch) || "\n" + var cur = lineObj.text.charAt(pos.ch) || "\n" var type = isWordChar(cur, helper) ? "w" : group && cur == "\n" ? "n" : !group || /\s/.test(cur) ? null : "p" if (group && !first && !type) { type = "s" } if (sawType && sawType != type) { - if (dir < 0) {dir = 1; moveOnce()} + if (dir < 0) {dir = 1; moveOnce(); pos.sticky = "after"} break } @@ -8118,8 +8639,8 @@ function findPosH(doc, pos, dir, unit, visually) { if (dir > 0 && !moveOnce(!first)) { break } } } - var result = skipAtomic(doc, Pos(line, ch), pos, origDir, true) - if (!cmp(pos, result)) { result.hitSide = true } + var result = skipAtomic(doc, pos, oldPos, origDir, true) + if (equalCursorPos(oldPos, result)) { result.hitSide = true } return result } @@ -8148,7 +8669,7 @@ function findPosV(cm, pos, dir, unit) { // CONTENTEDITABLE INPUT STYLE -var ContentEditableInput = function ContentEditableInput(cm) { +var ContentEditableInput = function(cm) { this.cm = cm this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null this.polling = new Delayed() @@ -8157,7 +8678,7 @@ var ContentEditableInput = function ContentEditableInput(cm) { this.readDOMTimeout = null }; -ContentEditableInput.prototype.init = function init (display) { +ContentEditableInput.prototype.init = function (display) { var this$1 = this; var input = this, cm = input.cm @@ -8167,9 +8688,7 @@ ContentEditableInput.prototype.init = function init (display) { on(div, "paste", function (e) { if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } // IE doesn't fire input events, so we schedule a read for the pasted content in this way - if (ie_version <= 11) { setTimeout(operation(cm, function () { - if (!input.pollContent()) { regChange(cm) } - }), 20) } + if (ie_version <= 11) { setTimeout(operation(cm, function () { return this$1.updateFromDOM(); }), 20) } }) on(div, "compositionstart", function (e) { @@ -8234,46 +8753,58 @@ ContentEditableInput.prototype.init = function init (display) { on(div, "cut", onCopyCut) }; -ContentEditableInput.prototype.prepareSelection = function prepareSelection$1 () { +ContentEditableInput.prototype.prepareSelection = function () { var result = prepareSelection(this.cm, false) result.focus = this.cm.state.focused return result }; -ContentEditableInput.prototype.showSelection = function showSelection (info, takeFocus) { +ContentEditableInput.prototype.showSelection = function (info, takeFocus) { if (!info || !this.cm.display.view.length) { return } if (info.focus || takeFocus) { this.showPrimarySelection() } this.showMultipleSelections(info) }; -ContentEditableInput.prototype.showPrimarySelection = function showPrimarySelection () { - var sel = window.getSelection(), prim = this.cm.doc.sel.primary() - var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset) - var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset) +ContentEditableInput.prototype.getSelection = function () { + return this.cm.display.wrapper.ownerDocument.getSelection() +}; + +ContentEditableInput.prototype.showPrimarySelection = function () { + var sel = this.getSelection(), cm = this.cm, prim = cm.doc.sel.primary() + var from = prim.from(), to = prim.to() + + if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) { + sel.removeAllRanges() + return + } + + var curAnchor = domToPos(cm, sel.anchorNode, sel.anchorOffset) + var curFocus = domToPos(cm, sel.focusNode, sel.focusOffset) if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && - cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && - cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) + cmp(minPos(curAnchor, curFocus), from) == 0 && + cmp(maxPos(curAnchor, curFocus), to) == 0) { return } - var start = posToDOM(this.cm, prim.from()) - var end = posToDOM(this.cm, prim.to()) - if (!start && !end) { return } - - var view = this.cm.display.view - var old = sel.rangeCount && sel.getRangeAt(0) - if (!start) { - start = {node: view[0].measure.map[2], offset: 0} - } else if (!end) { // FIXME dangerously hacky + var view = cm.display.view + var start = (from.line >= cm.display.viewFrom && posToDOM(cm, from)) || + {node: view[0].measure.map[2], offset: 0} + var end = to.line < cm.display.viewTo && posToDOM(cm, to) + if (!end) { var measure = view[view.length - 1].measure - var map$$1 = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map - end = {node: map$$1[map$$1.length - 1], offset: map$$1[map$$1.length - 2] - map$$1[map$$1.length - 3]} + var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map + end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]} + } + + if (!start || !end) { + sel.removeAllRanges() + return } - var rng + var old = sel.rangeCount && sel.getRangeAt(0), rng try { rng = range(start.node, start.offset, end.offset, end.node) } catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible if (rng) { - if (!gecko && this.cm.state.focused) { + if (!gecko && cm.state.focused) { sel.collapse(start.node, start.offset) if (!rng.collapsed) { sel.removeAllRanges() @@ -8289,7 +8820,7 @@ ContentEditableInput.prototype.showPrimarySelection = function showPrimarySelect this.rememberSelection() }; -ContentEditableInput.prototype.startGracePeriod = function startGracePeriod () { +ContentEditableInput.prototype.startGracePeriod = function () { var this$1 = this; clearTimeout(this.gracePeriod) @@ -8300,37 +8831,37 @@ ContentEditableInput.prototype.startGracePeriod = function startGracePeriod () { }, 20) }; -ContentEditableInput.prototype.showMultipleSelections = function showMultipleSelections (info) { +ContentEditableInput.prototype.showMultipleSelections = function (info) { removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors) removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection) }; -ContentEditableInput.prototype.rememberSelection = function rememberSelection () { - var sel = window.getSelection() +ContentEditableInput.prototype.rememberSelection = function () { + var sel = this.getSelection() this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset }; -ContentEditableInput.prototype.selectionInEditor = function selectionInEditor () { - var sel = window.getSelection() +ContentEditableInput.prototype.selectionInEditor = function () { + var sel = this.getSelection() if (!sel.rangeCount) { return false } var node = sel.getRangeAt(0).commonAncestorContainer return contains(this.div, node) }; -ContentEditableInput.prototype.focus = function focus () { +ContentEditableInput.prototype.focus = function () { if (this.cm.options.readOnly != "nocursor") { if (!this.selectionInEditor()) { this.showSelection(this.prepareSelection(), true) } this.div.focus() } }; -ContentEditableInput.prototype.blur = function blur () { this.div.blur() }; -ContentEditableInput.prototype.getField = function getField () { return this.div }; +ContentEditableInput.prototype.blur = function () { this.div.blur() }; +ContentEditableInput.prototype.getField = function () { return this.div }; -ContentEditableInput.prototype.supportsTouch = function supportsTouch () { return true }; +ContentEditableInput.prototype.supportsTouch = function () { return true }; -ContentEditableInput.prototype.receivedFocus = function receivedFocus () { +ContentEditableInput.prototype.receivedFocus = function () { var input = this if (this.selectionInEditor()) { this.pollSelection() } @@ -8346,26 +8877,38 @@ ContentEditableInput.prototype.receivedFocus = function receivedFocus () { this.polling.set(this.cm.options.pollInterval, poll) }; -ContentEditableInput.prototype.selectionChanged = function selectionChanged () { - var sel = window.getSelection() +ContentEditableInput.prototype.selectionChanged = function () { + var sel = this.getSelection() return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset }; -ContentEditableInput.prototype.pollSelection = function pollSelection () { - if (!this.composing && this.readDOMTimeout == null && !this.gracePeriod && this.selectionChanged()) { - var sel = window.getSelection(), cm = this.cm - this.rememberSelection() - var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset) - var head = domToPos(cm, sel.focusNode, sel.focusOffset) - if (anchor && head) { runInOp(cm, function () { - setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll) - if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true } - }) } +ContentEditableInput.prototype.pollSelection = function () { + if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) { return } + var sel = this.getSelection(), cm = this.cm + // On Android Chrome (version 56, at least), backspacing into an + // uneditable block element will put the cursor in that element, + // and then, because it's not editable, hide the virtual keyboard. + // Because Android doesn't allow us to actually detect backspace + // presses in a sane way, this code checks for when that happens + // and simulates a backspace press in this case. + if (android && chrome && this.cm.options.gutters.length && isInGutter(sel.anchorNode)) { + this.cm.triggerOnKeyDown({type: "keydown", keyCode: 8, preventDefault: Math.abs}) + this.blur() + this.focus() + return } + if (this.composing) { return } + this.rememberSelection() + var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset) + var head = domToPos(cm, sel.focusNode, sel.focusOffset) + if (anchor && head) { runInOp(cm, function () { + setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll) + if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true } + }) } }; -ContentEditableInput.prototype.pollContent = function pollContent () { +ContentEditableInput.prototype.pollContent = function () { if (this.readDOMTimeout != null) { clearTimeout(this.readDOMTimeout) this.readDOMTimeout = null @@ -8416,6 +8959,14 @@ ContentEditableInput.prototype.pollContent = function pollContent () { while (cutEnd < maxCutEnd && newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) { ++cutEnd } + // Try to move start of change to start of selection if ambiguous + if (newText.length == 1 && oldText.length == 1 && fromLine == from.line) { + while (cutFront && cutFront > from.ch && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) { + cutFront-- + cutEnd++ + } + } newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, "") newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, "") @@ -8428,21 +8979,21 @@ ContentEditableInput.prototype.pollContent = function pollContent () { } }; -ContentEditableInput.prototype.ensurePolled = function ensurePolled () { +ContentEditableInput.prototype.ensurePolled = function () { this.forceCompositionEnd() }; -ContentEditableInput.prototype.reset = function reset () { +ContentEditableInput.prototype.reset = function () { this.forceCompositionEnd() }; -ContentEditableInput.prototype.forceCompositionEnd = function forceCompositionEnd () { +ContentEditableInput.prototype.forceCompositionEnd = function () { if (!this.composing) { return } clearTimeout(this.readDOMTimeout) this.composing = null - if (!this.pollContent()) { regChange(this.cm) } + this.updateFromDOM() this.div.blur() this.div.focus() }; -ContentEditableInput.prototype.readFromDOMSoon = function readFromDOMSoon () { +ContentEditableInput.prototype.readFromDOMSoon = function () { var this$1 = this; if (this.readDOMTimeout != null) { return } @@ -8452,27 +9003,34 @@ ContentEditableInput.prototype.readFromDOMSoon = function readFromDOMSoon () { if (this$1.composing.done) { this$1.composing = null } else { return } } - if (this$1.cm.isReadOnly() || !this$1.pollContent()) - { runInOp(this$1.cm, function () { return regChange(this$1.cm); }) } + this$1.updateFromDOM() }, 80) }; -ContentEditableInput.prototype.setUneditable = function setUneditable (node) { +ContentEditableInput.prototype.updateFromDOM = function () { + var this$1 = this; + + if (this.cm.isReadOnly() || !this.pollContent()) + { runInOp(this.cm, function () { return regChange(this$1.cm); }) } +}; + +ContentEditableInput.prototype.setUneditable = function (node) { node.contentEditable = "false" }; -ContentEditableInput.prototype.onKeyPress = function onKeyPress (e) { +ContentEditableInput.prototype.onKeyPress = function (e) { + if (e.charCode == 0 || this.composing) { return } e.preventDefault() if (!this.cm.isReadOnly()) { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0) } }; -ContentEditableInput.prototype.readOnlyChanged = function readOnlyChanged (val) { +ContentEditableInput.prototype.readOnlyChanged = function (val) { this.div.contentEditable = String(val != "nocursor") }; -ContentEditableInput.prototype.onContextMenu = function onContextMenu () {}; -ContentEditableInput.prototype.resetPosition = function resetPosition () {}; +ContentEditableInput.prototype.onContextMenu = function () {}; +ContentEditableInput.prototype.resetPosition = function () {}; ContentEditableInput.prototype.needsContentAttribute = true @@ -8482,7 +9040,7 @@ function posToDOM(cm, pos) { var line = getLine(cm.doc, pos.line) var info = mapFromLineView(view, line, pos.line) - var order = getOrder(line), side = "left" + var order = getOrder(line, cm.doc.direction), side = "left" if (order) { var partPos = getBidiPartAt(order, pos.ch) side = partPos % 2 ? "right" : "left" @@ -8492,45 +9050,63 @@ function posToDOM(cm, pos) { return result } +function isInGutter(node) { + for (var scan = node; scan; scan = scan.parentNode) + { if (/CodeMirror-gutter-wrapper/.test(scan.className)) { return true } } + return false +} + function badPos(pos, bad) { if (bad) { pos.bad = true; } return pos } function domTextBetween(cm, from, to, fromLine, toLine) { - var text = "", closing = false, lineSep = cm.doc.lineSeparator() + var text = "", closing = false, lineSep = cm.doc.lineSeparator(), extraLinebreak = false function recognizeMarker(id) { return function (marker) { return marker.id == id; } } + function close() { + if (closing) { + text += lineSep + if (extraLinebreak) { text += lineSep } + closing = extraLinebreak = false + } + } + function addText(str) { + if (str) { + close() + text += str + } + } function walk(node) { if (node.nodeType == 1) { var cmText = node.getAttribute("cm-text") - if (cmText != null) { - if (cmText == "") { text += node.textContent.replace(/\u200b/g, "") } - else { text += cmText } + if (cmText) { + addText(cmText) return } - var markerID = node.getAttribute("cm-marker"), range$$1 + var markerID = node.getAttribute("cm-marker"), range if (markerID) { var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)) - if (found.length && (range$$1 = found[0].find())) - { text += getBetween(cm.doc, range$$1.from, range$$1.to).join(lineSep) } + if (found.length && (range = found[0].find(0))) + { addText(getBetween(cm.doc, range.from, range.to).join(lineSep)) } return } if (node.getAttribute("contenteditable") == "false") { return } + var isBlock = /^(pre|div|p|li|table|br)$/i.test(node.nodeName) + if (!/^br$/i.test(node.nodeName) && node.textContent.length == 0) { return } + + if (isBlock) { close() } for (var i = 0; i < node.childNodes.length; i++) { walk(node.childNodes[i]) } - if (/^(pre|div|p)$/i.test(node.nodeName)) - { closing = true } + + if (/^(pre|p)$/i.test(node.nodeName)) { extraLinebreak = true } + if (isBlock) { closing = true } } else if (node.nodeType == 3) { - var val = node.nodeValue - if (!val) { return } - if (closing) { - text += lineSep - closing = false - } - text += val + addText(node.nodeValue.replace(/\u200b/g, "").replace(/\u00a0/g, " ")) } } for (;;) { walk(from) if (from == to) { break } from = from.nextSibling + extraLinebreak = false } return text } @@ -8577,13 +9153,13 @@ function locateNodeInLineView(lineView, node, offset) { function find(textNode, topNode, offset) { for (var i = -1; i < (maps ? maps.length : 0); i++) { - var map$$1 = i < 0 ? measure.map : maps[i] - for (var j = 0; j < map$$1.length; j += 3) { - var curNode = map$$1[j + 2] + var map = i < 0 ? measure.map : maps[i] + for (var j = 0; j < map.length; j += 3) { + var curNode = map[j + 2] if (curNode == textNode || curNode == topNode) { var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]) - var ch = map$$1[j] + offset - if (offset < 0 || curNode != textNode) { ch = map$$1[j + (offset ? 1 : 0)] } + var ch = map[j] + offset + if (offset < 0 || curNode != textNode) { ch = map[j + (offset ? 1 : 0)] } return Pos(line, ch) } } @@ -8611,7 +9187,7 @@ function locateNodeInLineView(lineView, node, offset) { // TEXTAREA INPUT STYLE -var TextareaInput = function TextareaInput(cm) { +var TextareaInput = function(cm) { this.cm = cm // See input.poll and input.reset this.prevInput = "" @@ -8622,25 +9198,19 @@ var TextareaInput = function TextareaInput(cm) { this.pollingFast = false // Self-resetting timeout for the poller this.polling = new Delayed() - // Tracks when input.reset has punted to just putting a short - // string into the textarea instead of the full selection. - this.inaccurateSelection = false // Used to work around IE issue with selection being forgotten when focus moves away from textarea this.hasSelection = false this.composing = null }; -TextareaInput.prototype.init = function init (display) { +TextareaInput.prototype.init = function (display) { var this$1 = this; var input = this, cm = this.cm + this.createField(display) + var te = this.textarea - // Wraps and hides input textarea - var div = this.wrapper = hiddenTextarea() - // The semihidden textarea that is focused when the editor is - // focused, and receives input. - var te = this.textarea = div.firstChild - display.wrapper.insertBefore(div, display.wrapper.firstChild) + display.wrapper.insertBefore(this.wrapper, display.wrapper.firstChild) // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) if (ios) { te.style.width = "0px" } @@ -8661,12 +9231,6 @@ TextareaInput.prototype.init = function init (display) { if (signalDOMEvent(cm, e)) { return } if (cm.somethingSelected()) { setLastCopied({lineWise: false, text: cm.getSelections()}) - if (input.inaccurateSelection) { - input.prevInput = "" - input.inaccurateSelection = false - te.value = lastCopied.text.join("\n") - selectInput(te) - } } else if (!cm.options.lineWiseCopyCut) { return } else { @@ -8713,7 +9277,15 @@ TextareaInput.prototype.init = function init (display) { }) }; -TextareaInput.prototype.prepareSelection = function prepareSelection$1 () { +TextareaInput.prototype.createField = function (_display) { + // Wraps and hides input textarea + this.wrapper = hiddenTextarea() + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + this.textarea = this.wrapper.firstChild +}; + +TextareaInput.prototype.prepareSelection = function () { // Redraw the selection and/or cursor var cm = this.cm, display = cm.display, doc = cm.doc var result = prepareSelection(cm) @@ -8731,7 +9303,7 @@ TextareaInput.prototype.prepareSelection = function prepareSelection$1 () { return result }; -TextareaInput.prototype.showSelection = function showSelection (drawn) { +TextareaInput.prototype.showSelection = function (drawn) { var cm = this.cm, display = cm.display removeChildrenAndAdd(display.cursorDiv, drawn.cursors) removeChildrenAndAdd(display.selectionDiv, drawn.selection) @@ -8743,15 +9315,12 @@ TextareaInput.prototype.showSelection = function showSelection (drawn) { // Reset the input to correspond to the selection (or to be empty, // when not typing and nothing is selected) -TextareaInput.prototype.reset = function reset (typing) { - if (this.contextMenuPending) { return } - var minimal, selected, cm = this.cm, doc = cm.doc +TextareaInput.prototype.reset = function (typing) { + if (this.contextMenuPending || this.composing) { return } + var cm = this.cm if (cm.somethingSelected()) { this.prevInput = "" - var range$$1 = doc.sel.primary() - minimal = hasCopyEvent && - (range$$1.to().line - range$$1.from().line > 100 || (selected = cm.getSelection()).length > 1000) - var content = minimal ? "-" : selected || cm.getSelection() + var content = cm.getSelection() this.textarea.value = content if (cm.state.focused) { selectInput(this.textarea) } if (ie && ie_version >= 9) { this.hasSelection = content } @@ -8759,31 +9328,30 @@ TextareaInput.prototype.reset = function reset (typing) { this.prevInput = this.textarea.value = "" if (ie && ie_version >= 9) { this.hasSelection = null } } - this.inaccurateSelection = minimal }; -TextareaInput.prototype.getField = function getField () { return this.textarea }; +TextareaInput.prototype.getField = function () { return this.textarea }; -TextareaInput.prototype.supportsTouch = function supportsTouch () { return false }; +TextareaInput.prototype.supportsTouch = function () { return false }; -TextareaInput.prototype.focus = function focus () { +TextareaInput.prototype.focus = function () { if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { try { this.textarea.focus() } catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM } }; -TextareaInput.prototype.blur = function blur () { this.textarea.blur() }; +TextareaInput.prototype.blur = function () { this.textarea.blur() }; -TextareaInput.prototype.resetPosition = function resetPosition () { +TextareaInput.prototype.resetPosition = function () { this.wrapper.style.top = this.wrapper.style.left = 0 }; -TextareaInput.prototype.receivedFocus = function receivedFocus () { this.slowPoll() }; +TextareaInput.prototype.receivedFocus = function () { this.slowPoll() }; // Poll for input changes, using the normal rate of polling. This // runs as long as the editor is focused. -TextareaInput.prototype.slowPoll = function slowPoll () { +TextareaInput.prototype.slowPoll = function () { var this$1 = this; if (this.pollingFast) { return } @@ -8796,7 +9364,7 @@ TextareaInput.prototype.slowPoll = function slowPoll () { // When an event has just come in that is likely to add or change // something in the input textarea, we poll faster, to ensure that // the change appears on the screen quickly. -TextareaInput.prototype.fastPoll = function fastPoll () { +TextareaInput.prototype.fastPoll = function () { var missed = false, input = this input.pollingFast = true function p() { @@ -8813,7 +9381,7 @@ TextareaInput.prototype.fastPoll = function fastPoll () { // used). When nothing is selected, the cursor sits after previously // seen text (can be empty), which is stored in prevInput (we must // not reset the textarea when typing, because that breaks IME). -TextareaInput.prototype.poll = function poll () { +TextareaInput.prototype.poll = function () { var this$1 = this; var cm = this.cm, input = this.textarea, prevInput = this.prevInput @@ -8864,16 +9432,16 @@ TextareaInput.prototype.poll = function poll () { return true }; -TextareaInput.prototype.ensurePolled = function ensurePolled () { +TextareaInput.prototype.ensurePolled = function () { if (this.pollingFast && this.poll()) { this.pollingFast = false } }; -TextareaInput.prototype.onKeyPress = function onKeyPress () { +TextareaInput.prototype.onKeyPress = function () { if (ie && ie_version >= 9) { this.hasSelection = null } this.fastPoll() }; -TextareaInput.prototype.onContextMenu = function onContextMenu (e) { +TextareaInput.prototype.onContextMenu = function (e) { var input = this, cm = input.cm, display = cm.display, te = input.textarea var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop if (!pos || presto) { return } // Opera is difficult. @@ -8926,10 +9494,14 @@ TextareaInput.prototype.onContextMenu = function onContextMenu (e) { if (!ie || (ie && ie_version < 9)) { prepareSelectAllHack() } var i = 0, poll = function () { if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && - te.selectionEnd > 0 && input.prevInput == "\u200b") - { operation(cm, selectAll)(cm) } - else if (i++ < 10) { display.detectingSelectAll = setTimeout(poll, 500) } - else { display.input.reset() } + te.selectionEnd > 0 && input.prevInput == "\u200b") { + operation(cm, selectAll)(cm) + } else if (i++ < 10) { + display.detectingSelectAll = setTimeout(poll, 500) + } else { + display.selForContextMenu = null + display.input.reset() + } } display.detectingSelectAll = setTimeout(poll, 200) } @@ -8948,11 +9520,12 @@ TextareaInput.prototype.onContextMenu = function onContextMenu (e) { } }; -TextareaInput.prototype.readOnlyChanged = function readOnlyChanged (val) { +TextareaInput.prototype.readOnlyChanged = function (val) { if (!val) { this.reset() } + this.textarea.disabled = val == "nocursor" }; -TextareaInput.prototype.setUneditable = function setUneditable () {}; +TextareaInput.prototype.setUneditable = function () {}; TextareaInput.prototype.needsContentAttribute = false @@ -9008,7 +9581,7 @@ function fromTextArea(textarea, options) { } textarea.style.display = "none" - var cm = CodeMirror$1(function (node) { return textarea.parentNode.insertBefore(node, textarea.nextSibling); }, + var cm = CodeMirror(function (node) { return textarea.parentNode.insertBefore(node, textarea.nextSibling); }, options) return cm } @@ -9059,14 +9632,14 @@ function addLegacyProps(CodeMirror) { // EDITOR CONSTRUCTOR -defineOptions(CodeMirror$1) +defineOptions(CodeMirror) -addEditorMethods(CodeMirror$1) +addEditorMethods(CodeMirror) // Set up methods on CodeMirror's prototype to redirect to the editor's document. var dontDelegate = "iter insert remove copy getEditor constructor".split(" ") for (var prop in Doc.prototype) { if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) - { CodeMirror$1.prototype[prop] = (function(method) { + { CodeMirror.prototype[prop] = (function(method) { return function() {return method.apply(this.doc, arguments)} })(Doc.prototype[prop]) } } @@ -9074,39 +9647,39 @@ eventMixin(Doc) // INPUT HANDLING -CodeMirror$1.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput} +CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput} // MODE DEFINITION AND QUERYING // Extra arguments are stored as the mode's dependencies, which is // used by (legacy) mechanisms like loadmode.js to automatically // load a mode. (Preferred mechanism is the require/define calls.) -CodeMirror$1.defineMode = function(name/*, mode, …*/) { - if (!CodeMirror$1.defaults.mode && name != "null") { CodeMirror$1.defaults.mode = name } +CodeMirror.defineMode = function(name/*, mode, …*/) { + if (!CodeMirror.defaults.mode && name != "null") { CodeMirror.defaults.mode = name } defineMode.apply(this, arguments) } -CodeMirror$1.defineMIME = defineMIME +CodeMirror.defineMIME = defineMIME // Minimal default mode. -CodeMirror$1.defineMode("null", function () { return ({token: function (stream) { return stream.skipToEnd(); }}); }) -CodeMirror$1.defineMIME("text/plain", "null") +CodeMirror.defineMode("null", function () { return ({token: function (stream) { return stream.skipToEnd(); }}); }) +CodeMirror.defineMIME("text/plain", "null") // EXTENSIONS -CodeMirror$1.defineExtension = function (name, func) { - CodeMirror$1.prototype[name] = func +CodeMirror.defineExtension = function (name, func) { + CodeMirror.prototype[name] = func } -CodeMirror$1.defineDocExtension = function (name, func) { +CodeMirror.defineDocExtension = function (name, func) { Doc.prototype[name] = func } -CodeMirror$1.fromTextArea = fromTextArea +CodeMirror.fromTextArea = fromTextArea -addLegacyProps(CodeMirror$1) +addLegacyProps(CodeMirror) -CodeMirror$1.version = "5.22.0" +CodeMirror.version = "5.38.0" -return CodeMirror$1; +return CodeMirror; -}))); +}))); \ No newline at end of file diff --git a/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/mode/crystal/crystal.js b/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/mode/crystal/crystal.js similarity index 82% rename from src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/mode/crystal/crystal.js rename to src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/mode/crystal/crystal.js index e63627cee869..dada112da81d 100644 --- a/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/mode/crystal/crystal.js +++ b/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/mode/crystal/crystal.js @@ -29,26 +29,22 @@ var types = /^[A-Z_\u009F-\uFFFF][a-zA-Z0-9_\u009F-\uFFFF]*/; var keywords = wordRegExp([ "abstract", "alias", "as", "asm", "begin", "break", "case", "class", "def", "do", - "else", "elsif", "end", "ensure", "enum", "extend", "for", "fun", "if", "ifdef", + "else", "elsif", "end", "ensure", "enum", "extend", "for", "fun", "if", "include", "instance_sizeof", "lib", "macro", "module", "next", "of", "out", "pointerof", - "private", "protected", "rescue", "return", "require", "sizeof", "struct", - "super", "then", "type", "typeof", "union", "unless", "until", "when", "while", "with", - "yield", "__DIR__", "__FILE__", "__LINE__" + "private", "protected", "rescue", "return", "require", "select", "sizeof", "struct", + "super", "then", "type", "typeof", "uninitialized", "union", "unless", "until", "when", "while", "with", + "yield", "__DIR__", "__END_LINE__", "__FILE__", "__LINE__" ]); var atomWords = wordRegExp(["true", "false", "nil", "self"]); var indentKeywordsArray = [ "def", "fun", "macro", "class", "module", "struct", "lib", "enum", "union", - "if", "unless", "case", "while", "until", "begin", "then", - "do", - "for", "ifdef" + "do", "for" ]; var indentKeywords = wordRegExp(indentKeywordsArray); - var dedentKeywordsArray = [ - "end", - "else", "elsif", - "rescue", "ensure" - ]; + var indentExpressionKeywordsArray = ["if", "unless", "case", "while", "until", "begin", "then"]; + var indentExpressionKeywords = wordRegExp(indentExpressionKeywordsArray); + var dedentKeywordsArray = ["end", "else", "elsif", "rescue", "ensure"]; var dedentKeywords = wordRegExp(dedentKeywordsArray); var dedentPunctualsArray = ["\\)", "\\}", "\\]"]; var dedentPunctuals = new RegExp("^(?:" + dedentPunctualsArray.join("|") + ")$"); @@ -90,12 +86,15 @@ } else if (state.lastToken == ".") { return "property"; } else if (keywords.test(matched)) { - if (state.lastToken != "abstract" && indentKeywords.test(matched)) { - if (!(matched == "fun" && state.blocks.indexOf("lib") >= 0)) { + if (indentKeywords.test(matched)) { + if (!(matched == "fun" && state.blocks.indexOf("lib") >= 0) && !(matched == "def" && state.lastToken == "abstract")) { state.blocks.push(matched); state.currentIndent += 1; } - } else if (dedentKeywords.test(matched)) { + } else if ((state.lastStyle == "operator" || !state.lastStyle) && indentExpressionKeywords.test(matched)) { + state.blocks.push(matched); + state.currentIndent += 1; + } else if (matched == "end") { state.blocks.pop(); state.currentIndent -= 1; } @@ -124,12 +123,6 @@ return "variable-2"; } - // Global variables - if (stream.eat("$")) { - stream.eat(/[0-9]+|\?/) || stream.match(idents) || stream.match(types); - return "variable-3"; - } - // Constants and types if (stream.match(types)) { return "tag"; @@ -165,6 +158,9 @@ } else if (stream.match("%w")) { embed = false; delim = stream.next(); + } else if (stream.match("%q")) { + embed = false; + delim = stream.next(); } else { if(delim = stream.match(/^%([^\w\s=])/)) { delim = delim[1]; @@ -183,6 +179,11 @@ return chain(tokenQuote(delim, style, embed), stream, state); } + // Here Docs + if (matched = stream.match(/^<<-('?)([A-Z]\w*)\1/)) { + return chain(tokenHereDoc(matched[2], !matched[1]), stream, state) + } + // Characters if (stream.eat("'")) { stream.match(/^(?:[^']|\\(?:[befnrtv0'"]|[0-7]{3}|u(?:[0-9a-fA-F]{4}|\{[0-9a-fA-F]{1,6}\})))/); @@ -202,7 +203,7 @@ return "number"; } - if (stream.eat(/\d/)) { + if (stream.eat(/^\d/)) { stream.match(/^\d*(?:\.\d+)?(?:[eE][+-]?\d+)?/); return "number"; } @@ -339,7 +340,7 @@ return style; } - escaped = ch == "\\"; + escaped = embed && ch == "\\"; } else { stream.next(); escaped = false; @@ -350,12 +351,52 @@ }; } + function tokenHereDoc(phrase, embed) { + return function (stream, state) { + if (stream.sol()) { + stream.eatSpace() + if (stream.match(phrase)) { + state.tokenize.pop(); + return "string"; + } + } + + var escaped = false; + while (stream.peek()) { + if (!escaped) { + if (stream.match("{%", false)) { + state.tokenize.push(tokenMacro("%", "%")); + return "string"; + } + + if (stream.match("{{", false)) { + state.tokenize.push(tokenMacro("{", "}")); + return "string"; + } + + if (embed && stream.match("#{", false)) { + state.tokenize.push(tokenNest("#{", "}", "meta")); + return "string"; + } + + escaped = embed && stream.next() == "\\"; + } else { + stream.next(); + escaped = false; + } + } + + return "string"; + } + } + return { startState: function () { return { tokenize: [tokenBase], currentIndent: 0, lastToken: null, + lastStyle: null, blocks: [] }; }, @@ -366,6 +407,7 @@ if (style && style != "comment") { state.lastToken = token; + state.lastStyle = style; } return style; diff --git a/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/theme/neat.css b/src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/theme/neat.css similarity index 100% rename from src/compiler/crystal/tools/playground/public/vendor/codemirror-5.22.0/theme/neat.css rename to src/compiler/crystal/tools/playground/public/vendor/codemirror-5.38.0/theme/neat.css diff --git a/src/compiler/crystal/tools/playground/views/layout.html.ecr b/src/compiler/crystal/tools/playground/views/layout.html.ecr index 42d171136000..1496333548cf 100644 --- a/src/compiler/crystal/tools/playground/views/layout.html.ecr +++ b/src/compiler/crystal/tools/playground/views/layout.html.ecr @@ -3,8 +3,8 @@ - - + + @@ -51,9 +51,9 @@ - - - + + +