- Get the current version:
5.29.0.
+ Get the current version:
5.30.0.
You can see the
code,
read the
release notes,
or study the
user manual.
diff --git a/codemirror/lib/codemirror.js b/codemirror/lib/codemirror.js
index 27a3485..2f90d18 100644
--- a/codemirror/lib/codemirror.js
+++ b/codemirror/lib/codemirror.js
@@ -278,13 +278,18 @@ function skipExtendingChars(str, pos, dir) {
}
// Returns the value from the range [`from`; `to`] that satisfies
-// `pred` and is closest to `from`. Assumes that at least `to` satisfies `pred`.
+// `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 (Math.abs(from - to) <= 1) { return pred(from) ? from : to }
- var mid = Math.floor((from + to) / 2)
+ 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 }
+ else { from = mid + dir }
}
}
@@ -897,12 +902,12 @@ 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
}
}
@@ -1105,112 +1110,6 @@ function getOrder(line, direction) {
return order
}
-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) {
- 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
-}
-
// EVENT HANDLING
// Lightweight event framework. on/off also work on DOM nodes,
@@ -2719,15 +2618,22 @@ function pageScrollY() {
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)
@@ -2802,7 +2708,7 @@ function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) {
if (!order) { return get(sticky == "before" ? ch - 1 : ch, sticky == "before") }
function getBidi(ch, partPos, invert) {
- var part = order[partPos], right = (part.level % 2) != 0
+ var part = order[partPos], right = part.level == 1
return get(invert ? ch - 1 : ch, right != invert)
}
var partPos = getBidiPartAt(order, ch, sticky)
@@ -2860,77 +2766,146 @@ function coordsChar(cm, x, y) {
}
function wrappedLineExtent(cm, lineObj, preparedMeasure, y) {
- var measure = function (ch) { return intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, ch), "line"); }
+ y -= widgetTopHeight(lineObj)
var end = lineObj.text.length
- var begin = findFirst(function (ch) { return measure(ch - 1).bottom <= y; }, end, 0)
- end = findFirst(function (ch) { return measure(ch).top > y; }, begin, end)
+ 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 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)
}
+// 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
+}
+
function coordsCharInner(cm, lineObj, lineNo, x, y) {
+ // Move y into line-local coordinate space
y -= heightAtLine(lineObj)
- var begin = 0, end = lineObj.text.length
var preparedMeasure = prepareMeasureForLine(cm, lineObj)
- var pos
+ // 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) {
- if (cm.options.lineWrapping) {
- ;var assign;
- ((assign = wrappedLineExtent(cm, lineObj, preparedMeasure, y), begin = assign.begin, end = assign.end, assign))
- }
- pos = new Pos(lineNo, Math.floor(begin + (end - begin) / 2))
- var beginLeft = cursorCoords(cm, pos, "line", lineObj, preparedMeasure).left
- var dir = beginLeft < x ? 1 : -1
- var prevDiff, diff = beginLeft - x, prevPos
- var steps = Math.ceil((end - begin) / 4)
- outer: do {
- prevDiff = diff
- prevPos = pos
- var i = 0
- for (; i < steps; ++i) {
- var prevPos$1 = pos
- pos = moveVisually(cm, lineObj, pos, dir)
- if (pos == null || pos.ch < begin || end <= (pos.sticky == "before" ? pos.ch - 1 : pos.ch)) {
- pos = prevPos$1
- break outer
- }
- }
- diff = cursorCoords(cm, pos, "line", lineObj, preparedMeasure).left - x
- if (steps > 1) {
- var diff_change_per_step = Math.abs(diff - prevDiff) / steps
- steps = Math.min(steps, Math.ceil(Math.abs(diff) / diff_change_per_step))
- dir = diff < 0 ? 1 : -1
- }
- } while (diff != 0 && (steps > 1 || ((dir < 0) != (diff < 0) && (Math.abs(diff) <= Math.abs(prevDiff)))))
- if (Math.abs(diff) > Math.abs(prevDiff)) {
- if ((diff < 0) == (prevDiff < 0)) { throw new Error("Broke out of infinite loop in coordsCharInner") }
- pos = prevPos
+ 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
}
+ 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 {
- var ch = findFirst(function (ch) {
- var box = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, ch), "line")
- if (box.top > y) {
- // For the cursor stickiness
- end = Math.min(ch, end)
- return true
- }
- else if (box.bottom <= y) { return false }
- else if (box.left > x) { return true }
- else if (box.right < x) { return false }
- else { return (x - box.left < box.right - x) }
- }, begin, end)
- ch = skipExtendingChars(lineObj.text, ch, 1)
- pos = new Pos(lineNo, ch, ch == end ? "before" : "after")
- }
- var coords = cursorCoords(cm, pos, "line", lineObj, preparedMeasure)
- if (y < coords.top || coords.bottom < y) { pos.outside = true }
- pos.xRel = x < coords.left ? -1 : (x > coords.right ? 1 : 0)
- return pos
+ // (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;
+ 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
@@ -3056,12 +3031,14 @@ 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 }
+ 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()
@@ -3092,6 +3069,8 @@ 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, output) {
var display = cm.display, doc = cm.doc
@@ -3114,30 +3093,48 @@ function drawSelectionRange(cm, range, output) {
return charCoords(cm, Pos(line, ch), "div", lineObj, bias)
}
- iterateBidiSections(getOrder(lineObj, doc.direction), 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) }
+ var order = getOrder(lineObj, doc.direction)
+ iterateBidiSections(order, fromArg || 0, toArg == null ? lineLen : toArg, function (from, to, dir, i) {
+ var fromPos = coords(from, dir == "ltr" ? "left" : "right")
+ var toPos = coords(to - 1, dir == "ltr" ? "right" : "left")
+ if (dir == "ltr") {
+ var fromLeft = fromArg == null && from == 0 ? leftSide : fromPos.left
+ var toRight = toArg == null && to == lineLen ? rightSide : toPos.right
+ if (toPos.top - fromPos.top <= 3) { // Single line
+ add(fromLeft, toPos.top, toRight - fromLeft, toPos.bottom)
+ } else { // Multiple lines
+ add(fromLeft, fromPos.top, null, fromPos.bottom)
+ if (fromPos.bottom < toPos.top) { add(leftSide, fromPos.bottom, null, toPos.top) }
+ add(leftSide, toPos.top, toPos.right, toPos.bottom)
+ }
+ } else if (from < to) { // RTL
+ var fromRight = fromArg == null && from == 0 ? rightSide : fromPos.right
+ var toLeft = toArg == null && to == lineLen ? leftSide : toPos.left
+ if (toPos.top - fromPos.top <= 3) { // Single line
+ add(toLeft, toPos.top, fromRight - toLeft, toPos.bottom)
+ } else { // Multiple lines
+ var topLeft = leftSide
+ if (i) {
+ var topEnd = wrappedLineExtentChar(cm, lineObj, null, from).end
+ // The coordinates returned for an RTL wrapped space tend to
+ // be complete bogus, so try to skip that here.
+ topLeft = coords(topEnd - (/\s/.test(lineObj.text.charAt(topEnd - 1)) ? 2 : 1), "left").left
+ }
+ add(topLeft, fromPos.top, fromRight - topLeft, fromPos.bottom)
+ if (fromPos.bottom < toPos.top) { add(leftSide, fromPos.bottom, null, toPos.top) }
+ var botWidth = null
+ if (i < order.length - 1 || true) {
+ var botStart = wrappedLineExtentChar(cm, lineObj, null, to).begin
+ botWidth = coords(botStart, "right").right - toLeft
+ }
+ add(toLeft, toPos.top, botWidth, 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}
}
@@ -3767,7 +3764,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) {
@@ -3780,7 +3777,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)
@@ -5369,7 +5366,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})
}
@@ -6731,6 +6729,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) {
+ 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 = {
@@ -7292,7 +7396,7 @@ function leftButtonSelect(cm, event, start, behavior) {
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)
}
}
@@ -7344,13 +7448,52 @@ function leftButtonSelect(cm, event, start, behavior) {
on(document, "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)
+}
+
// Determines whether an event happened in the gutter, and fires the
// 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) }
@@ -7688,7 +7831,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
@@ -9472,7 +9615,7 @@ CodeMirror.fromTextArea = fromTextArea
addLegacyProps(CodeMirror)
-CodeMirror.version = "5.29.0"
+CodeMirror.version = "5.30.0"
return CodeMirror;
diff --git a/codemirror/mode/gfm/gfm.js b/codemirror/mode/gfm/gfm.js
index 689cd6e..471ae90 100644
--- a/codemirror/mode/gfm/gfm.js
+++ b/codemirror/mode/gfm/gfm.js
@@ -81,7 +81,7 @@ CodeMirror.defineMode("gfm", function(config, modeConfig) {
if (stream.sol() || state.ateSpace) {
state.ateSpace = false;
if (modeConfig.gitHubSpice !== false) {
- if(stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/)) {
+ if(stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?=.{0,6}\d)(?:[a-f0-9]{7,40}\b)/)) {
// User/Project@SHA
// User@SHA
// SHA
diff --git a/codemirror/mode/gfm/test.js b/codemirror/mode/gfm/test.js
index 9cda5c4..e713526 100644
--- a/codemirror/mode/gfm/test.js
+++ b/codemirror/mode/gfm/test.js
@@ -91,6 +91,9 @@
MT("userProjectSHAEmphasis",
"[em *foo ][em&link bar/hello@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2][em *]");
+ MT("wordSHA",
+ "ask for feedbac")
+
MT("num",
"foo [link #1] bar");
diff --git a/codemirror/mode/javascript/javascript.js b/codemirror/mode/javascript/javascript.js
index e724284..0a46dc7 100644
--- a/codemirror/mode/javascript/javascript.js
+++ b/codemirror/mode/javascript/javascript.js
@@ -28,7 +28,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
var jsKeywords = {
"if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
- "return": C, "break": C, "continue": C, "new": kw("new"), "delete": C, "throw": C, "debugger": C,
+ "return": C, "break": C, "continue": C, "new": kw("new"), "delete": C, "void": C, "throw": C, "debugger": C,
"var": kw("var"), "const": kw("var"), "let": kw("var"),
"function": kw("function"), "catch": kw("catch"),
"for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
@@ -398,7 +398,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
function expressionInner(type, noComma) {
if (cx.state.fatArrowAt == cx.stream.start) {
var body = noComma ? arrowBodyNoComma : arrowBody;
- if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext);
+ if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext);
else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
}
@@ -443,6 +443,11 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == ".") return cont(property, me);
if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) }
+ if (type == "regexp") {
+ cx.state.lastType = cx.marked = "operator"
+ cx.stream.backUp(cx.stream.pos - cx.stream.start - 1)
+ return cont(expr)
+ }
}
function quasi(type, value) {
if (type != "quasi") return pass();
@@ -491,6 +496,9 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
} else if (type == "variable" || cx.style == "keyword") {
cx.marked = "property";
if (value == "get" || value == "set") return cont(getterSetter);
+ var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params
+ if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false)))
+ cx.state.fatArrowAt = cx.stream.pos + m[0].length
return cont(afterprop);
} else if (type == "number" || type == "string") {
cx.marked = jsonldMode ? "property" : (cx.style + " property");
@@ -648,7 +656,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, maybetype, statement, popcontext);
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, functiondef)
}
- function funarg(type) {
+ function funarg(type, value) {
+ if (value == "@") cont(expression, funarg)
if (type == "spread" || type == "modifier") return cont(funarg);
return pass(pattern, maybetype, maybeAssign);
}
@@ -674,7 +683,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
cx.marked = "keyword";
return cont(classBody);
}
- if (type == "variable") {
+ if (type == "variable" || cx.style == "keyword") {
cx.marked = "property";
return cont(isTS ? classfield : functiondef, classBody);
}
@@ -736,7 +745,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
function expressionAllowed(stream, state, backUp) {
return state.tokenize == tokenBase &&
- /^(?:operator|sof|keyword c|case|new|export|default|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
+ /^(?:operator|sof|keyword [bc]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
(state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
}
diff --git a/codemirror/mode/javascript/test.js b/codemirror/mode/javascript/test.js
index f110b5c..7b0892c 100644
--- a/codemirror/mode/javascript/test.js
+++ b/codemirror/mode/javascript/test.js
@@ -236,6 +236,15 @@
" [keyword return] [number 2]",
"}")
+ MT("regexp_corner_case",
+ "[operator +]{} [operator /] [atom undefined];",
+ "[[[meta ...][string-2 /\\//] ]];",
+ "[keyword void] [string-2 /\\//];",
+ "[keyword do] [string-2 /\\//]; [keyword while] ([number 0]);",
+ "[keyword if] ([number 0]) {} [keyword else] [string-2 /\\//];",
+ "[string-2 `${][variable async][operator ++][string-2 }//`];",
+ "[string-2 `${]{} [operator /] [string-2 /\\//}`];")
+
var ts_mode = CodeMirror.getMode({indentUnit: 2}, "application/typescript")
function TS(name) {
test.mode(name, ts_mode, Array.prototype.slice.call(arguments, 1))
@@ -352,6 +361,9 @@
" [property constructor]([keyword readonly] [keyword private] [def x]) {}",
"}")
+ TS("arrow prop",
+ "({[property a]: [def p] [operator =>] [variable-2 p]})")
+
var jsonld_mode = CodeMirror.getMode(
{indentUnit: 2},
{name: "javascript", jsonld: true}
diff --git a/codemirror/mode/markdown/markdown.js b/codemirror/mode/markdown/markdown.js
index 53f9164..907059f 100644
--- a/codemirror/mode/markdown/markdown.js
+++ b/codemirror/mode/markdown/markdown.js
@@ -85,7 +85,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
var hrRE = /^([*\-_])(?:\s*\1){2,}\s*$/
, listRE = /^(?:[*\-+]|^[0-9]+([.)]))\s+/
- , taskListRE = /^\[(x| )\](?=\s)/ // Must follow listRE
+ , taskListRE = /^\[(x| )\](?=\s)/i // Must follow listRE
, atxHeaderRE = modeCfg.allowAtxHeaderWithoutSpace ? /^(#+)/ : /^(#+)(?: |$)/
, setextHeaderRE = /^ *(?:\={1,}|-{1,})\s*$/
, textRE = /^[^#!\[\]*_\\<>` "'(~:]+/
@@ -403,7 +403,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
if (state.taskList) {
- var taskOpen = stream.match(taskListRE, true)[1] !== "x";
+ var taskOpen = stream.match(taskListRE, true)[1] === " ";
if (taskOpen) state.taskOpen = true;
else state.taskClosed = true;
if (modeCfg.highlightFormatting) state.formatting = "task";
@@ -839,8 +839,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
},
indent: function(state, textAfter, line) {
- if (state.block == htmlBlock) return htmlMode.indent(state.htmlState, textAfter, line)
- if (state.localState) return state.localMode.indent(state.localState, textAfter, line)
+ if (state.block == htmlBlock && htmlMode.indent) return htmlMode.indent(state.htmlState, textAfter, line)
+ if (state.localState && state.localMode.indent) return state.localMode.indent(state.localState, textAfter, line)
return CodeMirror.Pass
},
diff --git a/codemirror/mode/meta.js b/codemirror/mode/meta.js
index c49bd6c..d5426a7 100644
--- a/codemirror/mode/meta.js
+++ b/codemirror/mode/meta.js
@@ -47,6 +47,7 @@
{name: "Embedded Javascript", mime: "application/x-ejs", mode: "htmlembedded", ext: ["ejs"]},
{name: "Embedded Ruby", mime: "application/x-erb", mode: "htmlembedded", ext: ["erb"]},
{name: "Erlang", mime: "text/x-erlang", mode: "erlang", ext: ["erl"]},
+ {name: "Esper", mime: "text/x-esper", mode: "sql"},
{name: "Factor", mime: "text/x-factor", mode: "factor", ext: ["factor"]},
{name: "FCL", mime: "text/x-fcl", mode: "fcl"},
{name: "Forth", mime: "text/x-forth", mode: "forth", ext: ["forth", "fth", "4th"]},
diff --git a/codemirror/mode/ruby/ruby.js b/codemirror/mode/ruby/ruby.js
index 68ebf12..874c183 100644
--- a/codemirror/mode/ruby/ruby.js
+++ b/codemirror/mode/ruby/ruby.js
@@ -286,7 +286,8 @@ CodeMirror.defineMode("ruby", function(config) {
},
electricInput: /^\s*(?:end|rescue|elsif|else|\})$/,
- lineComment: "#"
+ lineComment: "#",
+ fold: "indent"
};
});
diff --git a/codemirror/mode/soy/soy.js b/codemirror/mode/soy/soy.js
index ba16363..0e24457 100644
--- a/codemirror/mode/soy/soy.js
+++ b/codemirror/mode/soy/soy.js
@@ -87,7 +87,7 @@
kindTag: [],
soyState: [],
templates: null,
- variables: null,
+ variables: prepend(null, 'ij'),
scopes: null,
indent: 0,
quoteKind: null,
@@ -128,6 +128,13 @@
} else {
stream.skipToEnd();
}
+ if (!state.scopes) {
+ var paramRe = /@param\??\s+(\S+)/g;
+ var current = stream.current();
+ for (var match; (match = paramRe.exec(current)); ) {
+ state.variables = prepend(state.variables, match[1]);
+ }
+ }
return "comment";
case "templ-def":
@@ -187,6 +194,7 @@
if (stream.match(/^\/?}/)) {
if (state.tag == "/template" || state.tag == "/deltemplate") {
popscope(state);
+ state.variables = prepend(null, 'ij');
state.indent = 0;
} else {
if (state.tag == "/for" || state.tag == "/foreach") {
@@ -248,8 +256,14 @@
if (stream.match(/^\/\*/)) {
state.soyState.push("comment");
+ if (!state.scopes) {
+ state.variables = prepend(null, 'ij');
+ }
return "comment";
} else if (stream.match(stream.sol() ? /^\s*\/\/.*/ : /^\s+\/\/.*/)) {
+ if (!state.scopes) {
+ state.variables = prepend(null, 'ij');
+ }
return "comment";
} else if (stream.match(/^\{literal}/)) {
state.indent += config.indentUnit;
@@ -274,18 +288,18 @@
state.soyState.push("tag");
if (state.tag == "template" || state.tag == "deltemplate") {
state.soyState.push("templ-def");
- }
- if (state.tag == "call" || state.tag == "delcall") {
+ } else if (state.tag == "call" || state.tag == "delcall") {
state.soyState.push("templ-ref");
- }
- if (state.tag == "let") {
+ } else if (state.tag == "let") {
state.soyState.push("var-def");
- }
- if (state.tag == "for" || state.tag == "foreach") {
+ } else if (state.tag == "for" || state.tag == "foreach") {
state.scopes = prepend(state.scopes, state.variables);
state.soyState.push("var-def");
- }
- if (state.tag.match(/^@(?:param\??|inject)/)) {
+ } else if (state.tag == "namespace") {
+ if (!state.scopes) {
+ state.variables = prepend(null, 'ij');
+ }
+ } else if (state.tag.match(/^@(?:param\??|inject)/)) {
state.soyState.push("param-def");
}
return "keyword";
diff --git a/codemirror/mode/vue/vue.js b/codemirror/mode/vue/vue.js
index c0eab6b..84cd07c 100644
--- a/codemirror/mode/vue/vue.js
+++ b/codemirror/mode/vue/vue.js
@@ -32,13 +32,20 @@
var tagLanguages = {
script: [
["lang", /coffee(script)?/, "coffeescript"],
- ["type", /^(?:text|application)\/(?:x-)?coffee(?:script)?$/, "coffeescript"]
+ ["type", /^(?:text|application)\/(?:x-)?coffee(?:script)?$/, "coffeescript"],
+ ["lang", /^babel$/, "javascript"],
+ ["type", /^text\/babel$/, "javascript"],
+ ["type", /^text\/ecmascript-\d+$/, "javascript"]
],
style: [
["lang", /^stylus$/i, "stylus"],
["lang", /^sass$/i, "sass"],
+ ["lang", /^less$/i, "text/x-less"],
+ ["lang", /^scss$/i, "text/x-scss"],
["type", /^(text\/)?(x-)?styl(us)?$/i, "stylus"],
- ["type", /^text\/sass/i, "sass"]
+ ["type", /^text\/sass/i, "sass"],
+ ["type", /^(text\/)?(x-)?scss$/i, "text/x-scss"],
+ ["type", /^(text\/)?(x-)?less$/i, "text/x-less"]
],
template: [
["lang", /^vue-template$/i, "vue"],
diff --git a/codemirror/package.json b/codemirror/package.json
index 3f108eb..f251b23 100644
--- a/codemirror/package.json
+++ b/codemirror/package.json
@@ -1,6 +1,6 @@
{
"name": "codemirror",
- "version": "5.29.0",
+ "version": "5.30.0",
"main": "lib/codemirror.js",
"style": "lib/codemirror.css",
"description": "Full-featured in-browser code editor",
diff --git a/codemirror/src/display/operations.js b/codemirror/src/display/operations.js
index 6748af4..c300450 100644
--- a/codemirror/src/display/operations.js
+++ b/codemirror/src/display/operations.js
@@ -102,7 +102,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) {
@@ -115,7 +115,7 @@ function endOperation_W2(op) {
cm.display.maxLineChanged = false
}
- let takeFocus = op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus())
+ let takeFocus = op.focus && op.focus == activeElt()
if (op.preparedSelection)
cm.display.input.showSelection(op.preparedSelection, takeFocus)
if (op.updatedDisplay || op.startHeight != cm.doc.height)
diff --git a/codemirror/src/display/selection.js b/codemirror/src/display/selection.js
index d9b69d4..ba6feb8 100644
--- a/codemirror/src/display/selection.js
+++ b/codemirror/src/display/selection.js
@@ -1,7 +1,7 @@
import { Pos } from "../line/pos"
import { visualLine } from "../line/spans"
import { getLine } from "../line/utils_line"
-import { charCoords, cursorCoords, displayWidth, paddingH } from "../measurement/position_measurement"
+import { charCoords, cursorCoords, displayWidth, paddingH, wrappedLineExtentChar } from "../measurement/position_measurement"
import { getOrder, iterateBidiSections } from "../util/bidi"
import { elt } from "../util/dom"
@@ -9,13 +9,13 @@ export function updateSelection(cm) {
cm.display.input.showSelection(cm.display.input.prepareSelection())
}
-export function prepareSelection(cm, primary) {
+export function prepareSelection(cm, primary = true) {
let doc = cm.doc, result = {}
let curFragment = result.cursors = document.createDocumentFragment()
let selFragment = result.selection = document.createDocumentFragment()
for (let i = 0; i < doc.sel.ranges.length; i++) {
- if (primary === false && i == doc.sel.primIndex) continue
+ if (!primary && i == doc.sel.primIndex) continue
let range = doc.sel.ranges[i]
if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) continue
let collapsed = range.empty()
@@ -46,6 +46,8 @@ export 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, output) {
let display = cm.display, doc = cm.doc
@@ -70,30 +72,48 @@ function drawSelectionRange(cm, range, output) {
return charCoords(cm, Pos(line, ch), "div", lineObj, bias)
}
- iterateBidiSections(getOrder(lineObj, doc.direction), fromArg || 0, toArg == null ? lineLen : toArg, (from, to, dir) => {
- let 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") { let tmp = leftPos; leftPos = rightPos; rightPos = tmp }
- left = leftPos.left
- right = rightPos.right
+ let order = getOrder(lineObj, doc.direction)
+ iterateBidiSections(order, fromArg || 0, toArg == null ? lineLen : toArg, (from, to, dir, i) => {
+ let fromPos = coords(from, dir == "ltr" ? "left" : "right")
+ let toPos = coords(to - 1, dir == "ltr" ? "right" : "left")
+ if (dir == "ltr") {
+ let fromLeft = fromArg == null && from == 0 ? leftSide : fromPos.left
+ let toRight = toArg == null && to == lineLen ? rightSide : toPos.right
+ if (toPos.top - fromPos.top <= 3) { // Single line
+ add(fromLeft, toPos.top, toRight - fromLeft, toPos.bottom)
+ } else { // Multiple lines
+ add(fromLeft, fromPos.top, null, fromPos.bottom)
+ if (fromPos.bottom < toPos.top) add(leftSide, fromPos.bottom, null, toPos.top)
+ add(leftSide, toPos.top, toPos.right, toPos.bottom)
+ }
+ } else if (from < to) { // RTL
+ let fromRight = fromArg == null && from == 0 ? rightSide : fromPos.right
+ let toLeft = toArg == null && to == lineLen ? leftSide : toPos.left
+ if (toPos.top - fromPos.top <= 3) { // Single line
+ add(toLeft, toPos.top, fromRight - toLeft, toPos.bottom)
+ } else { // Multiple lines
+ let topLeft = leftSide
+ if (i) {
+ let topEnd = wrappedLineExtentChar(cm, lineObj, null, from).end
+ // The coordinates returned for an RTL wrapped space tend to
+ // be complete bogus, so try to skip that here.
+ topLeft = coords(topEnd - (/\s/.test(lineObj.text.charAt(topEnd - 1)) ? 2 : 1), "left").left
+ }
+ add(topLeft, fromPos.top, fromRight - topLeft, fromPos.bottom)
+ if (fromPos.bottom < toPos.top) add(leftSide, fromPos.bottom, null, toPos.top)
+ let botWidth = null
+ if (i < order.length - 1 || true) {
+ let botStart = wrappedLineExtentChar(cm, lineObj, null, to).begin
+ botWidth = coords(botStart, "right").right - toLeft
+ }
+ add(toLeft, toPos.top, botWidth, toPos.bottom)
+ }
}
- 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)
- }
- 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}
}
diff --git a/codemirror/src/edit/CodeMirror.js b/codemirror/src/edit/CodeMirror.js
index 1b2758e..0f0e589 100644
--- a/codemirror/src/edit/CodeMirror.js
+++ b/codemirror/src/edit/CodeMirror.js
@@ -143,7 +143,7 @@ function registerEventHandlers(cm) {
return dx * dx + dy * dy > 20 * 20
}
on(d.scroller, "touchstart", e => {
- if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e)) {
+ if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e) && !clickInGutter(cm, e)) {
d.input.ensurePolled()
clearTimeout(touchFinished)
let now = +new Date
diff --git a/codemirror/src/edit/main.js b/codemirror/src/edit/main.js
index 44779dc..4298c61 100644
--- a/codemirror/src/edit/main.js
+++ b/codemirror/src/edit/main.js
@@ -66,4 +66,4 @@ import { addLegacyProps } from "./legacy"
addLegacyProps(CodeMirror)
-CodeMirror.version = "5.29.0"
+CodeMirror.version = "5.30.0"
diff --git a/codemirror/src/edit/mouse_events.js b/codemirror/src/edit/mouse_events.js
index f816a4a..57159e3 100644
--- a/codemirror/src/edit/mouse_events.js
+++ b/codemirror/src/edit/mouse_events.js
@@ -8,6 +8,7 @@ import { eventInWidget } from "../measurement/widgets"
import { normalizeSelection, Range, Selection } from "../model/selection"
import { extendRange, extendSelection, replaceOneSelection, setSelection } from "../model/selection_updates"
import { captureRightClick, chromeOS, ie, ie_version, mac, webkit } from "../util/browser"
+import { getOrder, getBidiPartAt } from "../util/bidi"
import { activeElt } from "../util/dom"
import { e_button, e_defaultPrevented, e_preventDefault, e_target, hasHandler, off, on, signal, signalDOMEvent } from "../util/event"
import { dragAndDrop } from "../util/feature_detection"
@@ -269,7 +270,7 @@ function leftButtonSelect(cm, event, start, behavior) {
anchor = maxPos(oldRange.to(), range.head)
}
let ranges = startSel.ranges.slice(0)
- ranges[ourIndex] = new Range(clipPos(doc, anchor), head)
+ ranges[ourIndex] = bidiSimplify(cm, new Range(clipPos(doc, anchor), head))
setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse)
}
}
@@ -321,13 +322,50 @@ function leftButtonSelect(cm, event, start, behavior) {
on(document, "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) {
+ let {anchor, head} = range, anchorLine = getLine(cm.doc, anchor.line)
+ if (cmp(anchor, head) == 0 && anchor.sticky == head.sticky) return range
+ let order = getOrder(anchorLine)
+ if (!order) return range
+ let index = getBidiPartAt(order, anchor.ch, anchor.sticky), part = order[index]
+ if (part.from != anchor.ch && part.to != anchor.ch) return range
+ let 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)
+ let leftSide
+ if (head.line != anchor.line) {
+ leftSide = (head.line - anchor.line) * (cm.doc.direction == "ltr" ? 1 : -1) > 0
+ } else {
+ let headIndex = getBidiPartAt(order, head.ch, head.sticky)
+ let 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
+ }
+
+ let usePart = order[boundary + (leftSide ? -1 : 0)]
+ let from = leftSide == (usePart.level == 1)
+ let 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)
+}
+
// Determines whether an event happened in the gutter, and fires the
// handlers for the corresponding event.
function gutterEvent(cm, e, type, prevent) {
let 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)
diff --git a/codemirror/src/measurement/position_measurement.js b/codemirror/src/measurement/position_measurement.js
index 0f5f398..cc55fb4 100644
--- a/codemirror/src/measurement/position_measurement.js
+++ b/codemirror/src/measurement/position_measurement.js
@@ -1,4 +1,3 @@
-import { moveVisually } from "../input/movement"
import { buildLineContent, LineView } from "../line/line_data"
import { clipPos, Pos } from "../line/pos"
import { collapsedSpanAtEnd, heightAtLine, lineIsHidden, visualLine } from "../line/spans"
@@ -293,14 +292,21 @@ function pageScrollY() {
return window.pageYOffset || (document.documentElement || document.body).scrollTop
}
+function widgetTopHeight(lineObj) {
+ let height = 0
+ if (lineObj.widgets) for (let 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".
export function intoCoordSystem(cm, lineObj, rect, context, includeWidgets) {
- if (!includeWidgets && lineObj.widgets) for (let i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) {
- let size = widgetHeight(lineObj.widgets[i])
- rect.top += size; rect.bottom += size
+ if (!includeWidgets) {
+ let height = widgetTopHeight(lineObj)
+ rect.top += height; rect.bottom += height
}
if (context == "line") return rect
if (!context) context = "local"
@@ -376,7 +382,7 @@ export function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeig
if (!order) return get(sticky == "before" ? ch - 1 : ch, sticky == "before")
function getBidi(ch, partPos, invert) {
- let part = order[partPos], right = (part.level % 2) != 0
+ let part = order[partPos], right = part.level == 1
return get(invert ? ch - 1 : ch, right != invert)
}
let partPos = getBidiPartAt(order, ch, sticky)
@@ -434,76 +440,144 @@ export function coordsChar(cm, x, y) {
}
function wrappedLineExtent(cm, lineObj, preparedMeasure, y) {
- let measure = ch => intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, ch), "line")
+ y -= widgetTopHeight(lineObj)
let end = lineObj.text.length
- let begin = findFirst(ch => measure(ch - 1).bottom <= y, end, 0)
- end = findFirst(ch => measure(ch).top > y, begin, end)
+ let begin = findFirst(ch => measureCharPrepared(cm, preparedMeasure, ch - 1).bottom <= y, end, 0)
+ end = findFirst(ch => measureCharPrepared(cm, preparedMeasure, ch).top > y, begin, end)
return {begin, end}
}
export function wrappedLineExtentChar(cm, lineObj, preparedMeasure, target) {
+ if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj)
let targetTop = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, target), "line").top
return wrappedLineExtent(cm, lineObj, preparedMeasure, targetTop)
}
+// 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
+}
+
function coordsCharInner(cm, lineObj, lineNo, x, y) {
+ // Move y into line-local coordinate space
y -= heightAtLine(lineObj)
- let begin = 0, end = lineObj.text.length
let preparedMeasure = prepareMeasureForLine(cm, lineObj)
- let pos
+ // When directly calling `measureCharPrepared`, we have to adjust
+ // for the widgets at this line.
+ let widgetHeight = widgetTopHeight(lineObj)
+ let begin = 0, end = lineObj.text.length, ltr = true
+
let 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) {
- if (cm.options.lineWrapping) {
- ;({begin, end} = wrappedLineExtent(cm, lineObj, preparedMeasure, y))
- }
- pos = new Pos(lineNo, Math.floor(begin + (end - begin) / 2))
- let beginLeft = cursorCoords(cm, pos, "line", lineObj, preparedMeasure).left
- let dir = beginLeft < x ? 1 : -1
- let prevDiff, diff = beginLeft - x, prevPos
- let steps = Math.ceil((end - begin) / 4)
- outer: do {
- prevDiff = diff
- prevPos = pos
- let i = 0
- for (; i < steps; ++i) {
- let prevPos = pos
- pos = moveVisually(cm, lineObj, pos, dir)
- if (pos == null || pos.ch < begin || end <= (pos.sticky == "before" ? pos.ch - 1 : pos.ch)) {
- pos = prevPos
- break outer
- }
- }
- diff = cursorCoords(cm, pos, "line", lineObj, preparedMeasure).left - x
- if (steps > 1) {
- let diff_change_per_step = Math.abs(diff - prevDiff) / steps
- steps = Math.min(steps, Math.ceil(Math.abs(diff) / diff_change_per_step))
- dir = diff < 0 ? 1 : -1
- }
- } while (diff != 0 && (steps > 1 || ((dir < 0) != (diff < 0) && (Math.abs(diff) <= Math.abs(prevDiff)))))
- if (Math.abs(diff) > Math.abs(prevDiff)) {
- if ((diff < 0) == (prevDiff < 0)) throw new Error("Broke out of infinite loop in coordsCharInner")
- pos = prevPos
+ let 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.
+ let chAround = null, boxAround = null
+ let ch = findFirst(ch => {
+ let 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
}
+ return true
+ }, begin, end)
+
+ let 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
+ let 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 {
- let ch = findFirst(ch => {
- let box = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, ch), "line")
- if (box.top > y) {
- // For the cursor stickiness
- end = Math.min(ch, end)
- return true
- }
- else if (box.bottom <= y) return false
- else if (box.left > x) return true
- else if (box.right < x) return false
- else return (x - box.left < box.right - x)
- }, begin, end)
- ch = skipExtendingChars(lineObj.text, ch, 1)
- pos = new Pos(lineNo, ch, ch == end ? "before" : "after")
+ // (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
+ let coords = cursorCoords(cm, Pos(lineNo, ch, sticky), "line", lineObj, preparedMeasure)
+ baseX = coords.left
+ outside = y < coords.top || y >= coords.bottom
}
- let coords = cursorCoords(cm, pos, "line", lineObj, preparedMeasure)
- if (y < coords.top || coords.bottom < y) pos.outside = true
- pos.xRel = x < coords.left ? -1 : (x > coords.right ? 1 : 0)
- return pos
+
+ 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.
+ let index = findFirst(i => {
+ let 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)
+ let 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) {
+ let ltr = part.level != 1
+ let 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.
+ let {begin, end} = wrappedLineExtent(cm, lineObj, preparedMeasure, y)
+ let part = null, closestDist = null
+ for (let i = 0; i < order.length; i++) {
+ let p = order[i]
+ if (p.from >= end || p.to <= begin) continue
+ let ltr = p.level != 1
+ let 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
+ let 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
}
let measureText
diff --git a/codemirror/src/model/changes.js b/codemirror/src/model/changes.js
index 308dc6b..cfad529 100644
--- a/codemirror/src/model/changes.js
+++ b/codemirror/src/model/changes.js
@@ -260,9 +260,9 @@ function makeChangeSingleDocInEditor(cm, change, spans) {
export function replaceRange(doc, code, from, to, origin) {
if (!to) to = from
- if (cmp(to, from) < 0) { let tmp = to; to = from; from = tmp }
+ if (cmp(to, from) < 0) [from, to] = [to, from]
if (typeof code == "string") code = doc.splitLines(code)
- makeChange(doc, {from: from, to: to, text: code, origin: origin})
+ makeChange(doc, {from, to, text: code, origin})
}
// Rebasing/resetting history to deal with externally-sourced changes
diff --git a/codemirror/src/util/bidi.js b/codemirror/src/util/bidi.js
index e7429fc..c3e63f7 100644
--- a/codemirror/src/util/bidi.js
+++ b/codemirror/src/util/bidi.js
@@ -3,12 +3,12 @@ import { lst } from "./misc"
// BIDI HELPERS
export function iterateBidiSections(order, from, to, f) {
- if (!order) return f(from, to, "ltr")
+ if (!order) return f(from, to, "ltr", 0)
let found = false
for (let i = 0; i < order.length; ++i) {
let 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
}
}
diff --git a/codemirror/src/util/misc.js b/codemirror/src/util/misc.js
index 2ad1d0e..39661eb 100644
--- a/codemirror/src/util/misc.js
+++ b/codemirror/src/util/misc.js
@@ -134,12 +134,17 @@ export function skipExtendingChars(str, pos, dir) {
}
// Returns the value from the range [`from`; `to`] that satisfies
-// `pred` and is closest to `from`. Assumes that at least `to` satisfies `pred`.
+// `pred` and is closest to `from`. Assumes that at least `to`
+// satisfies `pred`. Supports `from` being greater than `to`.
export function findFirst(pred, from, to) {
+ // At any point we are certain `to` satisfies `pred`, don't know
+ // whether `from` does.
+ let dir = from > to ? -1 : 1
for (;;) {
- if (Math.abs(from - to) <= 1) return pred(from) ? from : to
- let mid = Math.floor((from + to) / 2)
+ if (from == to) return from
+ let 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
+ else from = mid + dir
}
}
diff --git a/codemirror/test/test.js b/codemirror/test/test.js
index 59b760d..96e989d 100644
--- a/codemirror/test/test.js
+++ b/codemirror/test/test.js
@@ -254,6 +254,11 @@ testCM("coordsCharBidi", function(cm) {
}
}, {lineNumbers: true});
+testCM("badBidiOptimization", function(cm) {
+ var coords = cm.charCoords(Pos(0, 34))
+ eqCharPos(cm.coordsChar({left: coords.right, top: coords.top + 2}), Pos(0, 34))
+}, {value: "----------
هل يمكنك اختيار مستوى قسط التأمين الذي ترغب بدفعه؟
"})
+
testCM("posFromIndex", function(cm) {
cm.setValue(
"This function should\n" +
@@ -1156,6 +1161,16 @@ testCM("measureWrappedEndOfLine", function(cm) {
}
}, {mode: "text/html", value: "0123456789abcde0123456789", lineWrapping: true}, ie_lt8 || opera_lt10);
+testCM("measureEndOfLineBidi", function(cm) {
+ eqCursorPos(cm.coordsChar({left: 5000, top: cm.charCoords(Pos(0, 0)).top}), Pos(0, 8, "after"))
+}, {value: "إإإإuuuuإإإإ"})
+
+testCM("measureWrappedBidiLevel2", function(cm) {
+ cm.setSize(cm.charCoords(Pos(0, 6), "editor").right + 60)
+ var c9 = cm.charCoords(Pos(0, 9))
+ eqCharPos(cm.coordsChar({left: c9.right - 1, top: c9.top + 1}), Pos(0, 9))
+}, {value: "foobar إإ إإ إإ إإ 555 بببببب", lineWrapping: true})
+
testCM("measureWrappedBeginOfLine", function(cm) {
if (phantom) return;
cm.setSize(null, "auto");
@@ -2468,6 +2483,23 @@ for (var i = 0; i < 5; ++i) {
}
*/
+testCM("rtl_wrapped_selection", function(cm) {
+ cm.setSelection(Pos(0, 10), Pos(0, 190))
+ is(byClassName(cm.getWrapperElement(), "CodeMirror-selected").length >= 3)
+}, {value: new Array(10).join(" فتي تم تضمينها فتي تم"), lineWrapping: true})
+
+testCM("bidi_wrapped_selection", function(cm) {
+ if (phantom) return
+ cm.setSize(cm.charCoords(Pos(0, 10), "editor").left)
+ cm.setSelection(Pos(0, 37), Pos(0, 80))
+ var blocks = byClassName(cm.getWrapperElement(), "CodeMirror-selected")
+ is(blocks.length >= 2)
+ is(blocks.length <= 3)
+ var boxTop = blocks[0].getBoundingClientRect(), boxBot = blocks[blocks.length - 1].getBoundingClientRect()
+ is(boxTop.left > cm.charCoords(Pos(0, 1)).right)
+ is(boxBot.right < cm.charCoords(Pos(0, cm.getLine(0).length - 2)).left)
+}, {value: "
مفتي11 تم تضمينهفتي تم تضمينها فتي تفتي تم تضمينها فتي تفتي تم تضمينها فتي تفتي تم تضمينها فتي تا فت10ي ت
", lineWrapping: true})
+
testCM("delete_wrapped", function(cm) {
makeItWrapAfter(cm, Pos(0, 2));
cm.doc.setCursor(Pos(0, 3, "after"));