Skip to content

Instantly share code, notes, and snippets.

@marijnh

marijnh/index.js Secret

Created July 9, 2020 12:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marijnh/8c180c05d0f70e8c725507a42b1a4b87 to your computer and use it in GitHub Desktop.
Save marijnh/8c180c05d0f70e8c725507a42b1a4b87 to your computer and use it in GitHub Desktop.
view/dist/index.js
import { MapMode, Facet, Text as Text$1, EditorSelection, ChangeSet, Transaction, CharCategory, EditorState, precedence, StateField, combineConfig } from '@codemirror/next/state';
import { StyleModule } from 'style-mod';
import { RangeValue, RangeSet } from '@codemirror/next/rangeset';
export { Range } from '@codemirror/next/rangeset';
import { Text, countColumn, findColumn, codePointAt } from '@codemirror/next/text';
import { keyName, base } from 'w3c-keyname';
let [nav, doc] = typeof navigator != "undefined"
? [navigator, document]
: [{ userAgent: "", vendor: "", platform: "" }, { documentElement: { style: {} } }];
const ie_edge = /Edge\/(\d+)/.exec(nav.userAgent);
const ie_upto10 = /MSIE \d/.test(nav.userAgent);
const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(nav.userAgent);
const ie = !!(ie_upto10 || ie_11up || ie_edge);
const gecko = !ie && /gecko\/(\d+)/i.test(nav.userAgent);
const chrome = !ie && /Chrome\/(\d+)/.exec(nav.userAgent);
const webkit = "webkitFontSmoothing" in doc.documentElement.style;
var browser = {
mac: /Mac/.test(nav.platform),
ie,
ie_version: ie_upto10 ? doc.documentMode || 6 : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0,
gecko,
gecko_version: gecko ? +(/Firefox\/(\d+)/.exec(nav.userAgent) || [0, 0])[1] : 0,
chrome: !!chrome,
chrome_version: chrome ? +chrome[1] : 0,
ios: !ie && /AppleWebKit/.test(nav.userAgent) && /Mobile\/\w+/.test(nav.userAgent),
android: /Android\b/.test(nav.userAgent),
webkit,
safari: /Apple Computer/.test(nav.vendor),
webkit_version: webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] : 0,
tabSize: doc.documentElement.style.tabSize != null ? "tab-size" : "-moz-tab-size"
};
function getSelection(root) {
return (root.getSelection ? root.getSelection() : document.getSelection());
}
// Work around Chrome issue https://bugs.chromium.org/p/chromium/issues/detail?id=447523
// (isCollapsed inappropriately returns true in shadow dom)
function selectionCollapsed(domSel) {
let collapsed = domSel.isCollapsed;
if (collapsed && browser.chrome && domSel.rangeCount && !domSel.getRangeAt(0).collapsed)
collapsed = false;
return collapsed;
}
function hasSelection(dom, selection) {
if (!selection.anchorNode)
return false;
try {
// Firefox will raise 'permission denied' errors when accessing
// properties of `sel.anchorNode` when it's in a generated CSS
// element.
return dom.contains(selection.anchorNode.nodeType == 3 ? selection.anchorNode.parentNode : selection.anchorNode);
}
catch (_) {
return false;
}
}
function clientRectsFor(dom) {
if (dom.nodeType == 3) {
let range = document.createRange();
range.setEnd(dom, dom.nodeValue.length);
range.setStart(dom, 0);
return range.getClientRects();
}
else if (dom.nodeType == 1) {
return dom.getClientRects();
}
else {
return [];
}
}
// Scans forward and backward through DOM positions equivalent to the
// given one to see if the two are in the same place (i.e. after a
// text node vs at the end of that text node)
function isEquivalentPosition(node, off, targetNode, targetOff) {
return targetNode ? (scanFor(node, off, targetNode, targetOff, -1) ||
scanFor(node, off, targetNode, targetOff, 1)) : false;
}
function domIndex(node) {
for (var index = 0;; index++) {
node = node.previousSibling;
if (!node)
return index;
}
}
function scanFor(node, off, targetNode, targetOff, dir) {
for (;;) {
if (node == targetNode && off == targetOff)
return true;
if (off == (dir < 0 ? 0 : maxOffset(node))) {
if (node.nodeName == "DIV")
return false;
let parent = node.parentNode;
if (!parent || parent.nodeType != 1)
return false;
off = domIndex(node) + (dir < 0 ? 0 : 1);
node = parent;
}
else if (node.nodeType == 1) {
node = node.childNodes[off + (dir < 0 ? -1 : 0)];
off = dir < 0 ? maxOffset(node) : 0;
}
else {
return false;
}
}
}
function maxOffset(node) {
return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length;
}
function flattenRect(rect, left) {
let x = left ? rect.left : rect.right;
return { left: x, right: x, top: rect.top, bottom: rect.bottom };
}
function windowRect(win) {
return { left: 0, right: win.innerWidth,
top: 0, bottom: win.innerHeight };
}
const ScrollSpace = 5;
function scrollRectIntoView(dom, rect) {
let doc = dom.ownerDocument, win = doc.defaultView;
for (let cur = dom.parentNode; cur;) {
if (cur.nodeType == 1) { // Element
let bounding, top = cur == document.body;
if (top) {
bounding = windowRect(win);
}
else {
if (cur.scrollHeight <= cur.clientHeight && cur.scrollWidth <= cur.clientWidth) {
cur = cur.parentNode;
continue;
}
let rect = cur.getBoundingClientRect();
// Make sure scrollbar width isn't included in the rectangle
bounding = { left: rect.left, right: rect.left + cur.clientWidth,
top: rect.top, bottom: rect.top + cur.clientHeight };
}
let moveX = 0, moveY = 0;
if (rect.top < bounding.top)
moveY = -(bounding.top - rect.top + ScrollSpace);
else if (rect.bottom > bounding.bottom)
moveY = rect.bottom - bounding.bottom + ScrollSpace;
if (rect.left < bounding.left)
moveX = -(bounding.left - rect.left + ScrollSpace);
else if (rect.right > bounding.right)
moveX = rect.right - bounding.right + ScrollSpace;
if (moveX || moveY) {
if (top) {
win.scrollBy(moveX, moveY);
}
else {
if (moveY) {
let start = cur.scrollTop;
cur.scrollTop += moveY;
moveY = cur.scrollTop - start;
}
if (moveX) {
let start = cur.scrollLeft;
cur.scrollLeft += moveX;
moveX = cur.scrollLeft - start;
}
rect = { left: rect.left - moveX, top: rect.top - moveY,
right: rect.right - moveX, bottom: rect.bottom - moveY };
}
}
if (top)
break;
cur = cur.parentNode;
}
else if (cur.nodeType == 11) { // A shadow root
cur = cur.host;
}
else {
break;
}
}
}
class DOMSelection {
constructor() {
this.anchorNode = null;
this.anchorOffset = 0;
this.focusNode = null;
this.focusOffset = 0;
}
eq(domSel) {
return this.anchorNode == domSel.anchorNode && this.anchorOffset == domSel.anchorOffset &&
this.focusNode == domSel.focusNode && this.focusOffset == domSel.focusOffset;
}
set(domSel) {
this.anchorNode = domSel.anchorNode;
this.anchorOffset = domSel.anchorOffset;
this.focusNode = domSel.focusNode;
this.focusOffset = domSel.focusOffset;
}
}
let preventScrollSupported = null;
// Feature-detects support for .focus({preventScroll: true}), and uses
// a fallback kludge when not supported.
function focusPreventScroll(dom) {
if (dom.setActive)
return dom.setActive(); // in IE
if (preventScrollSupported)
return dom.focus(preventScrollSupported);
let stack = [];
for (let cur = dom; cur; cur = cur.parentNode) {
stack.push(cur, cur.scrollTop, cur.scrollLeft);
if (cur == cur.ownerDocument)
break;
}
dom.focus(preventScrollSupported == null ? {
get preventScroll() {
preventScrollSupported = { preventScroll: true };
return true;
}
} : undefined);
if (!preventScrollSupported) {
preventScrollSupported = false;
for (let i = 0; i < stack.length;) {
let elt = stack[i++], top = stack[i++], left = stack[i++];
if (elt.scrollTop != top)
elt.scrollTop = top;
if (elt.scrollLeft != left)
elt.scrollLeft = left;
}
}
}
class DOMPos {
constructor(node, offset, precise = true) {
this.node = node;
this.offset = offset;
this.precise = precise;
}
static before(dom, precise) { return new DOMPos(dom.parentNode, domIndex(dom), precise); }
static after(dom, precise) { return new DOMPos(dom.parentNode, domIndex(dom) + 1, precise); }
}
const none = [];
class ContentView {
constructor() {
this.parent = null;
this.dom = null;
this.dirty = 2 /* Node */;
}
get editorView() {
if (!this.parent)
throw new Error("Accessing view in orphan content view");
return this.parent.editorView;
}
get overrideDOMText() { return null; }
get posAtStart() {
return this.parent ? this.parent.posBefore(this) : 0;
}
get posAtEnd() {
return this.posAtStart + this.length;
}
posBefore(view) {
let pos = this.posAtStart;
for (let child of this.children) {
if (child == view)
return pos;
pos += child.length + child.breakAfter;
}
throw new RangeError("Invalid child in posBefore");
}
posAfter(view) {
return this.posBefore(view) + view.length;
}
// Will return a rectangle directly before (when side < 0), after
// (side > 0) or directly on (when the browser supports it) the
// given position.
coordsAt(_pos, _side) { return null; }
sync(track) {
if (this.dirty & 2 /* Node */) {
let parent = this.dom, pos = null;
for (let child of this.children) {
if (child.dirty) {
let next = pos ? pos.nextSibling : parent.firstChild;
if (next && !child.dom && !ContentView.get(next))
child.reuseDOM(next);
child.sync(track);
child.dirty = 0 /* Not */;
}
if (track && track.node == parent && pos != child.dom)
track.written = true;
syncNodeInto(parent, pos, child.dom);
pos = child.dom;
}
let next = pos ? pos.nextSibling : parent.firstChild;
if (next && track && track.node == parent)
track.written = true;
while (next)
next = rm(next);
}
else if (this.dirty & 1 /* Child */) {
for (let child of this.children)
if (child.dirty) {
child.sync(track);
child.dirty = 0 /* Not */;
}
}
}
reuseDOM(_dom) { return false; }
localPosFromDOM(node, offset) {
let after;
if (node == this.dom) {
after = this.dom.childNodes[offset];
}
else {
let bias = maxOffset(node) == 0 ? 0 : offset == 0 ? -1 : 1;
for (;;) {
let parent = node.parentNode;
if (parent == this.dom)
break;
if (bias == 0 && parent.firstChild != parent.lastChild) {
if (node == parent.firstChild)
bias = -1;
else
bias = 1;
}
node = parent;
}
if (bias < 0)
after = node;
else
after = node.nextSibling;
}
if (after == this.dom.firstChild)
return 0;
while (after && !ContentView.get(after))
after = after.nextSibling;
if (!after)
return this.length;
for (let i = 0, pos = 0;; i++) {
let child = this.children[i];
if (child.dom == after)
return pos;
pos += child.length + child.breakAfter;
}
}
domBoundsAround(from, to, offset = 0) {
let fromI = -1, fromStart = -1, toI = -1, toEnd = -1;
for (let i = 0, pos = offset; i < this.children.length; i++) {
let child = this.children[i], end = pos + child.length;
if (pos < from && end > to)
return child.domBoundsAround(from, to, pos);
if (end >= from && fromI == -1) {
fromI = i;
fromStart = pos;
}
if (end >= to && toI == -1) {
toI = i;
toEnd = end;
break;
}
pos = end + child.breakAfter;
}
return { from: fromStart, to: toEnd, startDOM: (fromI ? this.children[fromI - 1].dom.nextSibling : null) || this.dom.firstChild, endDOM: toI < this.children.length - 1 ? this.children[toI + 1].dom : null };
}
// FIXME track precise dirty ranges, to avoid full DOM sync on every touched node?
markDirty(andParent = false) {
if (this.dirty & 2 /* Node */)
return;
this.dirty |= 2 /* Node */;
this.markParentsDirty(andParent);
}
markParentsDirty(childList) {
for (let parent = this.parent; parent; parent = parent.parent) {
if (childList)
parent.dirty |= 2 /* Node */;
if (parent.dirty & 1 /* Child */)
return;
parent.dirty |= 1 /* Child */;
childList = false;
}
}
setParent(parent) {
if (this.parent != parent) {
this.parent = parent;
if (this.dirty)
this.markParentsDirty(true);
}
}
setDOM(dom) {
this.dom = dom;
dom.cmView = this;
}
get rootView() {
for (let v = this;;) {
let parent = v.parent;
if (!parent)
return v;
v = parent;
}
}
replaceChildren(from, to, children = none) {
this.markDirty();
for (let i = from; i < to; i++)
this.children[i].parent = null;
this.children.splice(from, to - from, ...children);
for (let i = 0; i < children.length; i++)
children[i].setParent(this);
}
ignoreMutation(_rec) { return false; }
ignoreEvent(_event) { return false; }
childCursor(pos = this.length) {
return new ChildCursor(this.children, pos, this.children.length);
}
childPos(pos, bias = 1) {
return this.childCursor().findPos(pos, bias);
}
toString() {
let name = this.constructor.name.replace("View", "");
return name + (this.children.length ? "(" + this.children.join() + ")" :
this.length ? "[" + (name == "Text" ? this.text : this.length) + "]" : "") +
(this.breakAfter ? "#" : "");
}
static get(node) { return node.cmView; }
}
ContentView.prototype.breakAfter = 0;
// Remove a DOM node and return its next sibling.
function rm(dom) {
let next = dom.nextSibling;
dom.parentNode.removeChild(dom);
return next;
}
function syncNodeInto(parent, after, dom) {
let next = after ? after.nextSibling : parent.firstChild;
if (dom.parentNode == parent)
while (next != dom)
next = rm(next);
else
parent.insertBefore(dom, next);
}
class ChildCursor {
constructor(children, pos, i) {
this.children = children;
this.pos = pos;
this.i = i;
this.off = 0;
}
findPos(pos, bias = 1) {
for (;;) {
if (pos > this.pos || pos == this.pos &&
(bias > 0 || this.i == 0 || this.children[this.i - 1].breakAfter)) {
this.off = pos - this.pos;
return this;
}
let next = this.children[--this.i];
this.pos -= next.length + next.breakAfter;
}
}
}
function combineAttrs(source, target) {
for (let name in source) {
if (name == "class" && target.class)
target.class += " " + source.class;
else if (name == "style" && target.style)
target.style += ";" + source.style;
else
target[name] = source[name];
}
return target;
}
function attrsEq(a, b) {
if (a == b)
return true;
if (!a || !b)
return false;
let keysA = Object.keys(a), keysB = Object.keys(b);
if (keysA.length != keysB.length)
return false;
for (let key of keysA) {
if (keysB.indexOf(key) == -1 || a[key] !== b[key])
return false;
}
return true;
}
function updateAttrs(dom, prev, attrs) {
if (prev)
for (let name in prev)
if (!(attrs && name in attrs))
dom.removeAttribute(name);
if (attrs)
for (let name in attrs)
if (!(prev && prev[name] == attrs[name]))
dom.setAttribute(name, attrs[name]);
}
const none$1 = [];
class InlineView extends ContentView {
match(_other) { return false; }
get children() { return none$1; }
getSide() { return 0; }
}
const MaxJoinLen = 256;
class TextView extends InlineView {
constructor(text, tagName, clss, attrs) {
super();
this.text = text;
this.tagName = tagName;
this.attrs = attrs;
this.textDOM = null;
this.class = clss;
}
get length() { return this.text.length; }
createDOM(textDOM) {
let tagName = this.tagName || (this.attrs || this.class ? "span" : null);
this.textDOM = textDOM || document.createTextNode(this.text);
if (tagName) {
let dom = document.createElement(tagName);
dom.appendChild(this.textDOM);
if (this.class)
dom.className = this.class;
if (this.attrs)
for (let name in this.attrs)
dom.setAttribute(name, this.attrs[name]);
this.setDOM(dom);
}
else {
this.setDOM(this.textDOM);
}
}
sync(track) {
if (!this.dom)
this.createDOM();
if (this.textDOM.nodeValue != this.text) {
if (track && track.node == this.textDOM)
track.written = true;
this.textDOM.nodeValue = this.text;
let dom = this.dom;
if (this.textDOM != dom && (this.dom.firstChild != this.textDOM || dom.lastChild != this.textDOM)) {
while (dom.firstChild)
dom.removeChild(dom.firstChild);
dom.appendChild(this.textDOM);
}
}
}
reuseDOM(dom) {
if (dom.nodeType != 3)
return false;
this.createDOM(dom);
return true;
}
merge(from, to = this.length, source = null) {
if (source &&
(!(source instanceof TextView) ||
source.tagName != this.tagName || source.class != this.class ||
!attrsEq(source.attrs, this.attrs) || this.length - (to - from) + source.length > MaxJoinLen))
return false;
this.text = this.text.slice(0, from) + (source ? source.text : "") + this.text.slice(to);
this.markDirty();
return true;
}
slice(from, to = this.length) {
return new TextView(this.text.slice(from, to), this.tagName, this.class, this.attrs);
}
localPosFromDOM(node, offset) {
return node == this.textDOM ? offset : offset ? this.text.length : 0;
}
domAtPos(pos) { return new DOMPos(this.textDOM, pos); }
domBoundsAround(_from, _to, offset) {
return { from: offset, to: offset + this.length, startDOM: this.dom, endDOM: this.dom.nextSibling };
}
coordsAt(pos, side) {
return textCoords(this.textDOM, pos, side, this.length);
}
}
function textCoords(text, pos, side, length) {
let from = pos, to = pos, flatten = 0;
if (pos == 0 && side < 0 || pos == length && side >= 0) {
if (!(browser.chrome || browser.gecko)) { // These browsers reliably return valid rectangles for empty ranges
if (pos) {
from--;
flatten = 1;
} // FIXME this is wrong in RTL text
else {
to++;
flatten = -1;
}
}
}
else {
if (side < 0)
from--;
else
to++;
}
let range = document.createRange();
range.setEnd(text, to);
range.setStart(text, from);
let rect = range.getBoundingClientRect();
return flatten ? flattenRect(rect, flatten < 0) : rect;
}
// Also used for collapsed ranges that don't have a placeholder widget!
class WidgetView extends InlineView {
constructor(widget, length, side, open) {
super();
this.widget = widget;
this.length = length;
this.side = side;
this.open = open;
}
static create(widget, length, side, open = 0) {
return new (widget.customView || WidgetView)(widget, length, side, open);
}
slice(from, to = this.length) { return WidgetView.create(this.widget, to - from, this.side); }
sync() {
if (!this.dom || !this.widget.updateDOM(this.dom)) {
this.setDOM(this.widget.toDOM(this.editorView));
this.dom.contentEditable = "false";
}
}
getSide() { return this.side; }
merge(from, to = this.length, source = null) {
if (source) {
if (!(source instanceof WidgetView) || !source.open ||
from > 0 && !(source.open & 1 /* Start */) ||
to < this.length && !(source.open & 2 /* End */))
return false;
if (!this.widget.compare(source.widget))
throw new Error("Trying to merge incompatible widgets");
}
this.length = from + (source ? source.length : 0) + (this.length - to);
return true;
}
match(other) {
if (other.length == this.length && other instanceof WidgetView && other.side == this.side) {
if (this.widget.constructor == other.widget.constructor) {
if (!this.widget.eq(other.widget.value))
this.markDirty(true);
this.widget = other.widget;
return true;
}
}
return false;
}
ignoreMutation() { return true; }
ignoreEvent(event) { return this.widget.ignoreEvent(event); }
get overrideDOMText() {
if (this.length == 0)
return Text.empty;
let top = this;
while (top.parent)
top = top.parent;
let view = top.editorView, text = view && view.state.doc, start = this.posAtStart;
return text ? text.slice(start, start + this.length) : Text.empty;
}
domAtPos(pos) {
return pos == 0 ? DOMPos.before(this.dom) : DOMPos.after(this.dom, pos == this.length);
}
domBoundsAround() { return null; }
coordsAt(pos, _side) {
let rects = this.dom.getClientRects(), rect = null;
for (let i = pos > 0 ? rects.length - 1 : 0;; i += (pos > 0 ? -1 : 1)) {
rect = rects[i];
if (pos > 0 ? i == 0 : i == rects.length - 1 || rect.top < rect.bottom)
break;
}
return rect;
}
}
class CompositionView extends WidgetView {
domAtPos(pos) { return new DOMPos(this.widget.value.text, pos); }
sync() { if (!this.dom)
this.setDOM(this.widget.toDOM(this.editorView)); }
localPosFromDOM(node, offset) {
return !offset ? 0 : node.nodeType == 3 ? Math.min(offset, this.length) : this.length;
}
ignoreMutation() { return false; }
get overrideDOMText() { return null; }
coordsAt(pos, side) { return textCoords(this.widget.value.text, pos, side, this.length); }
}
/// Widgets added to the content are described by subclasses of this
/// class. This makes it possible to delay creating of the DOM
/// structure for a widget until it is needed, and to avoid redrawing
/// widgets even when the decorations that define them are recreated.
/// `T` can be a type of value passed to instances of the widget type.
class WidgetType {
/// Create an instance of this widget type.
constructor(
/// @internal
value) {
this.value = value;
}
/// Compare this instance to another instance of the same class. By
/// default, it'll compare the instances' parameters with `===`.
eq(value) { return this.value === value; }
/// Update a DOM element created by a widget of the same type but
/// with a different value to reflect this widget. May return true
/// to indicate that it could update, false to indicate it couldn't
/// (in which case the widget will be redrawn). The default
/// implementation just returns false.
updateDOM(_dom) { return false; }
/// @internal
compare(other) {
return this == other || this.constructor == other.constructor && this.eq(other.value);
}
/// The estimated height this widget will have, to be used when
/// estimating the height of content that hasn't been drawn. May
/// return -1 to indicate you don't know. The default implementation
/// returns -1.
get estimatedHeight() { return -1; }
/// Can be used to configure which kinds of events inside the widget
/// should be ignored by the editor. The default is to ignore all
/// events.
ignoreEvent(_event) { return true; }
//// @internal
get customView() { return null; }
}
/// The different types of blocks that can occur in an editor view.
var BlockType;
(function (BlockType) {
/// A line of text.
BlockType[BlockType["Text"] = 0] = "Text";
/// A block widget associated with the position after it.
BlockType[BlockType["WidgetBefore"] = 1] = "WidgetBefore";
/// A block widget associated with the position before it.
BlockType[BlockType["WidgetAfter"] = 2] = "WidgetAfter";
/// A block widget [replacing](#view.Decoration^replace) a range of content.
BlockType[BlockType["WidgetRange"] = 3] = "WidgetRange";
})(BlockType || (BlockType = {}));
/// A decoration provides information on how to draw or style a piece
/// of content. You'll usually use it wrapped in a
/// [`Range`](#rangeset.Range), which adds a start and
/// end position.
class Decoration extends RangeValue {
/// @internal
constructor(
/// @internal
startSide,
/// @internal
endSide,
/// @internal
widget,
/// The config object used to create this decoration.
spec) {
super();
this.startSide = startSide;
this.endSide = endSide;
this.widget = widget;
this.spec = spec;
}
/// @internal
get point() { return false; }
/// @internal
get heightRelevant() { return false; }
/// Create a mark decoration, which influences the styling of the
/// text in its range.
static mark(spec) {
return new MarkDecoration(spec);
}
/// Create a widget decoration, which adds an element at the given
/// position.
static widget(spec) {
let side = spec.side || 0;
if (spec.block)
side += (200000000 /* BigBlock */ + 1) * (side > 0 ? 1 : -1);
return new PointDecoration(spec, side, side, !!spec.block, spec.widget || null, false);
}
/// Create a replace decoration which replaces the given range with
/// a widget, or simply hides it.
static replace(spec) {
let block = !!spec.block;
let { start, end } = getInclusive(spec);
let startSide = block ? -200000000 /* BigBlock */ * (start ? 2 : 1) : 100000000 /* BigInline */ * (start ? -1 : 1);
let endSide = block ? 200000000 /* BigBlock */ * (end ? 2 : 1) : 100000000 /* BigInline */ * (end ? 1 : -1);
return new PointDecoration(spec, startSide, endSide, block, spec.widget || null, true);
}
/// Create a line decoration, which can add DOM attributes to the
/// line starting at the given position.
static line(spec) {
return new LineDecoration(spec);
}
/// Build a [`DecorationSet`](#view.DecorationSet) from the given
/// decorated range or ranges.
static set(of, sort = false) {
return RangeSet.of(of, sort);
}
/// @internal
hasHeight() { return this.widget ? this.widget.estimatedHeight > -1 : false; }
}
/// The empty set of decorations.
Decoration.none = RangeSet.empty;
class MarkDecoration extends Decoration {
constructor(spec) {
let { start, end } = getInclusive(spec);
super(100000000 /* BigInline */ * (start ? -1 : 1), 100000000 /* BigInline */ * (end ? 1 : -1), null, spec);
}
eq(other) {
return this == other ||
other instanceof MarkDecoration &&
this.spec.tagName == other.spec.tagName &&
this.spec.class == other.spec.class &&
attrsEq(this.spec.attributes || null, other.spec.attributes || null);
}
range(from, to = from) {
if (from >= to)
throw new RangeError("Mark decorations may not be empty");
return super.range(from, to);
}
}
class LineDecoration extends Decoration {
constructor(spec) {
super(-100000000 /* BigInline */, -100000000 /* BigInline */, null, spec);
}
get point() { return true; }
eq(other) {
return other instanceof LineDecoration && attrsEq(this.spec.attributes, other.spec.attributes);
}
range(from, to = from) {
if (to != from)
throw new RangeError("Line decoration ranges must be zero-length");
return super.range(from, to);
}
}
LineDecoration.prototype.mapMode = MapMode.TrackBefore;
class PointDecoration extends Decoration {
constructor(spec, startSide, endSide, block, widget, isReplace) {
super(startSide, endSide, widget, spec);
this.block = block;
this.isReplace = isReplace;
this.mapMode = !block ? MapMode.TrackDel : startSide < 0 ? MapMode.TrackBefore : MapMode.TrackAfter;
}
get point() { return true; }
// Only relevant when this.block == true
get type() {
return this.startSide < this.endSide ? BlockType.WidgetRange
: this.startSide < 0 ? BlockType.WidgetBefore : BlockType.WidgetAfter;
}
get heightRelevant() { return this.block || !!this.widget && this.widget.estimatedHeight >= 5; }
eq(other) {
return other instanceof PointDecoration &&
widgetsEq(this.widget, other.widget) &&
this.block == other.block &&
this.startSide == other.startSide && this.endSide == other.endSide;
}
range(from, to = from) {
if (this.isReplace && (from > to || (from == to && this.startSide > 0 && this.endSide < 0)))
throw new RangeError("Invalid range for replacement decoration");
if (!this.isReplace && to != from)
throw new RangeError("Widget decorations can only have zero-length ranges");
return super.range(from, to);
}
}
function getInclusive(spec) {
let { inclusiveStart: start, inclusiveEnd: end } = spec;
if (start == null)
start = spec.inclusive;
if (end == null)
end = spec.inclusive;
return { start: start || false, end: end || false };
}
function widgetsEq(a, b) {
return a == b || !!(a && b && a.compare(b));
}
const MinRangeGap = 4;
function addRange(from, to, ranges) {
let last = ranges.length - 1;
if (last >= 0 && ranges[last] + MinRangeGap > from)
ranges[last] = Math.max(ranges[last], to);
else
ranges.push(from, to);
}
const theme = Facet.define({ combine: strs => strs.join(" ") });
const darkTheme = Facet.define({ combine: values => values.indexOf(true) > -1 });
const baseThemeID = StyleModule.newName();
const baseLightThemeID = StyleModule.newName();
const baseDarkThemeID = StyleModule.newName();
function buildTheme(mainID, spec) {
let styles = Object.create(null);
for (let prop in spec) {
let selector = prop.split(/\s*,\s*/).map(piece => {
let id = mainID, narrow;
if (id == baseThemeID && (narrow = /^(.*?)@(light|dark)$/.exec(piece))) {
id = narrow[2] == "dark" ? baseDarkThemeID : baseLightThemeID;
piece = narrow[1];
}
let parts = piece.split("."), selector = "." + id + (parts[0] == "wrap" ? "" : " ");
for (let i = 1; i <= parts.length; i++)
selector += ".cm-" + parts.slice(0, i).join("-");
return selector;
}).join(", ");
styles[selector] = spec[prop];
}
return new StyleModule(styles, { generateClasses: false });
}
/// Create a set of CSS class names for the given theme selector,
/// which can be added to a DOM element within an editor to make
/// themes able to style it. Theme selectors can be single words or
/// words separated by dot characters. In the latter case, the
/// returned classes combine those that match the full name and those
/// that match some prefix—for example `"panel.search"` will match
/// both the theme styles specified as `"panel.search"` and those with
/// just `"panel"`. More specific theme styles (with more dots) take
/// precedence.
function themeClass(selector) {
let parts = selector.split("."), result = "";
for (let i = 1; i <= parts.length; i++)
result += (result ? " " : "") + "cm-" + parts.slice(0, i).join("-");
return result;
}
const baseTheme = buildTheme(baseThemeID, {
wrap: {
position: "relative !important",
boxSizing: "border-box",
"&.cm-focused": {
// FIXME it would be great if we could directly use the browser's
// default focus outline, but it appears we can't, so this tries to
// approximate that
outline_fallback: "1px dotted #212121",
outline: "5px auto -webkit-focus-ring-color"
},
display: "flex !important",
flexDirection: "column"
},
scroller: {
display: "flex !important",
alignItems: "flex-start !important",
fontFamily: "monospace",
lineHeight: 1.4,
height: "100%",
overflowX: "auto"
},
content: {
margin: 0,
flexGrow: 2,
minHeight: "100%",
display: "block",
whiteSpace: "pre",
boxSizing: "border-box",
padding: "4px 0",
outline: "none"
},
"content@light": { caretColor: "black" },
"content@dark": { caretColor: "white" },
line: {
display: "block",
padding: "0 2px 0 4px"
},
button: {
verticalAlign: "middle",
color: "inherit",
fontSize: "70%",
padding: ".2em 1em",
borderRadius: "3px"
},
"button@light": {
backgroundImage: "linear-gradient(#eff1f5, #d9d9df)",
border: "1px solid #888",
"&:active": {
backgroundImage: "linear-gradient(#b4b4b4, #d0d3d6)"
}
},
"button@dark": {
backgroundImage: "linear-gradient(#555, #111)",
border: "1px solid #888",
"&:active": {
backgroundImage: "linear-gradient(#111, #333)"
}
},
textfield: {
verticalAlign: "middle",
color: "inherit",
fontSize: "70%",
border: "1px solid silver",
padding: ".2em .5em"
},
"textfield@light": {
backgroundColor: "white"
},
"textfield@dark": {
border: "1px solid #555",
backgroundColor: "inherit"
},
secondarySelection: {
backgroundColor_fallback: "#3297FD",
color_fallback: "white !important",
backgroundColor: "Highlight",
color: "HighlightText !important"
},
secondaryCursor: {
display: "inline-block",
verticalAlign: "text-top",
width: 0,
height: "1.15em",
margin: "0 -0.7px -.7em"
},
"secondaryCursor@light": { borderLeft: "1.4px solid #555" },
"secondaryCursor@dark": { borderLeft: "1.4px solid #ddd" }
});
const LineClass = themeClass("line");
class LineView extends ContentView {
constructor() {
super(...arguments);
this.children = [];
this.length = 0;
this.prevAttrs = undefined;
this.attrs = null;
this.breakAfter = 0;
}
// Consumes source
merge(from, to, source, takeDeco) {
if (source) {
if (!(source instanceof LineView))
return false;
if (!this.dom)
source.transferDOM(this); // Reuse source.dom when appropriate
}
if (takeDeco)
this.setDeco(source ? source.attrs : null);
let elts = source ? source.children : [];
let cur = this.childCursor();
let { i: toI, off: toOff } = cur.findPos(to, 1);
let { i: fromI, off: fromOff } = cur.findPos(from, -1);
let dLen = from - to;
for (let view of elts)
dLen += view.length;
this.length += dLen;
// Both from and to point into the same text view
if (fromI == toI && fromOff) {
let start = this.children[fromI];
// Maybe just update that view and be done
if (elts.length == 1 && start.merge(fromOff, toOff, elts[0]))
return true;
if (elts.length == 0) {
start.merge(fromOff, toOff, null);
return true;
}
// Otherwise split it, so that we don't have to worry about aliasing front/end afterwards
let after = start.slice(toOff);
if (after.merge(0, 0, elts[elts.length - 1]))
elts[elts.length - 1] = after;
else
elts.push(after);
toI++;
toOff = 0;
}
// Make sure start and end positions fall on node boundaries
// (fromOff/toOff are no longer used after this), and that if the
// start or end of the elts can be merged with adjacent nodes,
// this is done
if (toOff) {
let end = this.children[toI];
if (elts.length && end.merge(0, toOff, elts[elts.length - 1]))
elts.pop();
else
end.merge(0, toOff, null);
}
else if (toI < this.children.length && elts.length &&
this.children[toI].merge(0, 0, elts[elts.length - 1])) {
elts.pop();
}
if (fromOff) {
let start = this.children[fromI];
if (elts.length && start.merge(fromOff, undefined, elts[0]))
elts.shift();
else
start.merge(fromOff, undefined, null);
fromI++;
}
else if (fromI && elts.length && this.children[fromI - 1].merge(this.children[fromI - 1].length, undefined, elts[0])) {
elts.shift();
}
// Then try to merge any mergeable nodes at the start and end of
// the changed range
while (fromI < toI && elts.length && this.children[toI - 1].match(elts[elts.length - 1])) {
elts.pop();
toI--;
}
while (fromI < toI && elts.length && this.children[fromI].match(elts[0])) {
elts.shift();
fromI++;
}
// And if anything remains, splice the child array to insert the new elts
if (elts.length || fromI != toI)
this.replaceChildren(fromI, toI, elts);
return true;
}
split(at) {
let end = new LineView;
end.breakAfter = this.breakAfter;
if (this.length == 0)
return end;
let { i, off } = this.childPos(at);
if (off) {
end.append(this.children[i].slice(off));
this.children[i].merge(off, undefined, null);
i++;
}
for (let j = i; j < this.children.length; j++)
end.append(this.children[j]);
while (i > 0 && this.children[i - 1].length == 0) {
this.children[i - 1].parent = null;
i--;
}
this.children.length = i;
this.markDirty();
this.length = at;
return end;
}
transferDOM(other) {
if (!this.dom)
return;
other.setDOM(this.dom);
other.prevAttrs = this.prevAttrs === undefined ? this.attrs : this.prevAttrs;
this.prevAttrs = undefined;
this.dom = null;
}
setDeco(attrs) {
if (!attrsEq(this.attrs, attrs)) {
if (this.dom) {
this.prevAttrs = this.attrs;
this.markDirty();
}
this.attrs = attrs;
}
}
// Only called when building a line view in ContentBuilder
append(child) {
this.children.push(child);
child.setParent(this);
this.length += child.length;
}
// Only called when building a line view in ContentBuilder
addLineDeco(deco) {
let attrs = deco.spec.attributes;
if (attrs)
this.attrs = combineAttrs(attrs, this.attrs || {});
}
domAtPos(pos) {
let i = 0;
for (let off = 0; i < this.children.length; i++) {
let child = this.children[i], end = off + child.length;
if (end == off && child.getSide() <= 0)
continue;
if (pos > off && pos < end && child.dom.parentNode == this.dom)
return child.domAtPos(pos - off);
if (pos <= off)
break;
off = end;
}
for (; i > 0; i--) {
let before = this.children[i - 1].dom;
if (before.parentNode == this.dom)
return DOMPos.after(before);
}
return new DOMPos(this.dom, 0);
}
// FIXME might need another hack to work around Firefox's behavior
// of not actually displaying the cursor even though it's there in
// the DOM
sync(track) {
if (!this.dom) {
this.setDOM(document.createElement("div"));
this.dom.className = LineClass;
this.prevAttrs = this.attrs ? null : undefined;
}
if (this.prevAttrs !== undefined) {
updateAttrs(this.dom, this.prevAttrs, this.attrs);
this.dom.classList.add(LineClass);
this.prevAttrs = undefined;
}
super.sync(track);
let last = this.dom.lastChild;
if (!last || (last.nodeName != "BR" && !(ContentView.get(last) instanceof TextView))) {
let hack = document.createElement("BR");
hack.cmIgnore = true;
this.dom.appendChild(hack);
}
}
measureTextSize() {
if (this.children.length == 0 || this.length > 20)
return null;
let totalWidth = 0;
for (let child of this.children) {
if (!(child instanceof TextView))
return null;
let rects = clientRectsFor(child.dom);
if (rects.length != 1)
return null;
totalWidth += rects[0].width;
}
return { lineHeight: this.dom.getBoundingClientRect().height, charWidth: totalWidth / this.length };
}
coordsAt(pos, side) {
for (let off = 0, i = 0; i < this.children.length; i++) {
let child = this.children[i], end = off + child.length;
if (end != off && (side <= 0 || end == this.length ? end >= pos : end > pos))
return child.coordsAt(pos - off, side);
off = end;
}
return this.dom.lastChild.getBoundingClientRect();
}
match(_other) { return false; }
get type() { return BlockType.Text; }
static find(docView, pos) {
for (let i = 0, off = 0;; i++) {
let block = docView.children[i], end = off + block.length;
if (end >= pos) {
if (block instanceof LineView)
return block;
if (block.length)
return null;
}
off = end + block.breakAfter;
}
}
}
const none$2 = [];
class BlockWidgetView extends ContentView {
constructor(widget, length, type,
// This is set by the builder and used to distinguish between
// adjacent widgets and parts of the same widget when calling
// `merge`. It's kind of silly that it's an instance variable, but
// it's hard to route there otherwise.
open = 0) {
super();
this.widget = widget;
this.length = length;
this.type = type;
this.open = open;
this.breakAfter = 0;
}
merge(from, to, source) {
if (!(source instanceof BlockWidgetView) || !source.open ||
from > 0 && !(source.open & 1 /* Start */) ||
to < this.length && !(source.open & 2 /* End */))
return false;
if (!this.widget.compare(source.widget))
throw new Error("Trying to merge an open widget with an incompatible node");
this.length = from + source.length + (this.length - to);
return true;
}
domAtPos(pos) {
return pos == 0 ? DOMPos.before(this.dom) : DOMPos.after(this.dom, pos == this.length);
}
split(at) {
let len = this.length - at;
this.length = at;
return new BlockWidgetView(this.widget, len, this.type);
}
get children() { return none$2; }
sync() {
if (!this.dom || !this.widget.updateDOM(this.dom)) {
this.setDOM(this.widget.toDOM(this.editorView));
this.dom.contentEditable = "false";
}
}
get overrideDOMText() {
return this.parent ? this.parent.view.state.doc.slice(this.posAtStart, this.posAtEnd) : Text$1.empty;
}
domBoundsAround() { return null; }
match(other) {
if (other instanceof BlockWidgetView && other.type == this.type &&
other.widget.constructor == this.widget.constructor) {
if (!other.widget.eq(this.widget.value))
this.markDirty(true);
this.widget = other.widget;
this.length = other.length;
this.breakAfter = other.breakAfter;
return true;
}
return false;
}
}
class ContentBuilder {
constructor(doc, pos, end) {
this.doc = doc;
this.pos = pos;
this.end = end;
this.content = [];
this.curLine = null;
this.breakAtStart = 0;
this.text = "";
this.textOff = 0;
this.cursor = doc.iter();
this.skip = pos;
}
posCovered() {
if (this.content.length == 0)
return !this.breakAtStart && this.doc.lineAt(this.pos).from != this.pos;
let last = this.content[this.content.length - 1];
return !last.breakAfter && !(last instanceof BlockWidgetView && last.type == BlockType.WidgetBefore);
}
getLine() {
if (!this.curLine)
this.content.push(this.curLine = new LineView);
return this.curLine;
}
addWidget(view) {
this.curLine = null;
this.content.push(view);
}
finish() {
if (!this.posCovered())
this.getLine();
}
buildText(length, tagName, clss, attrs, _ranges) {
while (length > 0) {
if (this.textOff == this.text.length) {
let { value, lineBreak, done } = this.cursor.next(this.skip);
this.skip = 0;
if (done)
throw new Error("Ran out of text content when drawing inline views");
if (lineBreak) {
if (!this.posCovered())
this.getLine();
if (this.content.length)
this.content[this.content.length - 1].breakAfter = 1;
else
this.breakAtStart = 1;
this.curLine = null;
length--;
continue;
}
else {
this.text = value;
this.textOff = 0;
}
}
let take = Math.min(this.text.length - this.textOff, length);
this.getLine().append(new TextView(this.text.slice(this.textOff, this.textOff + take), tagName, clss, attrs));
length -= take;
this.textOff += take;
}
}
span(from, to, active) {
let tagName = null, clss = null;
let attrs = null;
for (let { spec } of active) {
if (spec.tagName)
tagName = spec.tagName;
if (spec.class)
clss = clss ? clss + " " + spec.class : spec.class;
if (spec.attributes)
for (let name in spec.attributes) {
let value = spec.attributes[name];
if (value == null)
continue;
if (name == "class") {
clss = clss ? clss + " " + value : value;
}
else {
if (!attrs)
attrs = {};
if (name == "style" && attrs.style)
value = attrs.style + ";" + value;
attrs[name] = value;
}
}
}
this.buildText(to - from, tagName, clss, attrs, active);
this.pos = to;
}
point(from, to, deco, openStart, openEnd) {
let open = (openStart ? 1 /* Start */ : 0) | (openEnd ? 2 /* End */ : 0);
let len = to - from;
if (deco instanceof PointDecoration) {
if (deco.block) {
let { type } = deco;
if (type == BlockType.WidgetAfter && !this.posCovered())
this.getLine();
this.addWidget(new BlockWidgetView(deco.widget || new NullWidget("div"), len, type, open));
}
else {
this.getLine().append(WidgetView.create(deco.widget || new NullWidget("span"), len, deco.startSide, open));
}
}
else if (this.doc.lineAt(this.pos).from == this.pos) { // Line decoration
this.getLine().addLineDeco(deco);
}
if (len) {
// Advance the iterator past the replaced content
if (this.textOff + len <= this.text.length) {
this.textOff += len;
}
else {
this.skip += len - (this.text.length - this.textOff);
this.text = "";
this.textOff = 0;
}
this.pos = to;
}
}
static build(text, from, to, decorations) {
let builder = new ContentBuilder(text, from, to);
RangeSet.spans(decorations, from, to, builder);
builder.finish();
return builder;
}
}
class NullWidget extends WidgetType {
toDOM() { return document.createElement(this.value); }
updateDOM(elt) { return elt.nodeName.toLowerCase() == this.value; }
}
/// Used to indicate [text direction](#view.EditorView.textDirection).
var Direction;
(function (Direction) {
// (These are chosen to match the base levels, in bidi algorithm
// terms, of spans in that direction.)
Direction[Direction["LTR"] = 0] = "LTR";
Direction[Direction["RTL"] = 1] = "RTL";
})(Direction || (Direction = {}));
const LTR = Direction.LTR, RTL = Direction.RTL;
// Decode a string with each type encoded as log2(type)
function dec(str) {
let result = [];
for (let i = 0; i < str.length; i++)
result.push(1 << +str[i]);
return result;
}
// Character types for codepoints 0 to 0xf8
const LowTypes = dec("88888888888888888888888888888888888666888888787833333333337888888000000000000000000000000008888880000000000000000000000000088888888888888888888888888888888888887866668888088888663380888308888800000000000000000000000800000000000000000000000000000008");
// Character types for codepoints 0x600 to 0x6f9
const ArabicTypes = dec("4444448826627288999999999992222222222222222222222222222222222222222222222229999999999999999999994444444444644222822222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222999999949999999229989999223333333333");
function charType(ch) {
return ch <= 0xf7 ? LowTypes[ch] :
0x590 <= ch && ch <= 0x5f4 ? 2 /* R */ :
0x600 <= ch && ch <= 0x6f9 ? ArabicTypes[ch - 0x600] :
0x6ee <= ch && ch <= 0x8ac ? 4 /* AL */ :
0x2000 <= ch && ch <= 0x200b ? 256 /* NI */ :
ch == 0x200c ? 256 /* NI */ : 1 /* L */;
}
const BidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;
class BidiSpan {
constructor(from, to, level) {
this.from = from;
this.to = to;
this.level = level;
}
get dir() { return this.level % 2 ? RTL : LTR; }
side(end, dir) { return (this.dir == dir) == end ? this.to : this.from; }
static find(order, index, level, assoc) {
let maybe = -1;
for (let i = 0; i < order.length; i++) {
let span = order[i];
if (span.from <= index && span.to >= index) {
if (span.level == level)
return i;
// When multiple spans match, if assoc != 0, take the one that
// covers that side, otherwise take the one with the minimum
// level.
if (maybe < 0 || (assoc != 0 ? (assoc < 0 ? span.from < index : span.to > index) : order[maybe].level > span.level))
maybe = i;
}
}
if (maybe < 0)
throw new RangeError("Index out of range");
return maybe;
}
}
// Reused array of character types
const types = [];
function computeOrder(line, direction) {
let len = line.length, outerType = direction == LTR ? 1 /* L */ : 2 /* R */;
if (!line || outerType == 1 /* L */ && !BidiRE.test(line))
return trivialOrder(len);
// W1. Examine each non-spacing mark (NSM) in the level run, and
// change the type of the NSM to the type of the previous
// character. If the NSM is at the start of the level run, it will
// get the type of sor.
// W2. Search backwards from each instance of a European number
// until the first strong type (R, L, AL, or sor) is found. If an
// AL is found, change the type of the European number to Arabic
// number.
// W3. Change all ALs to R.
// (Left after this: L, R, EN, AN, ET, CS, NI)
for (let i = 0, prev = outerType, prevStrong = outerType; i < len; i++) {
let type = charType(line.charCodeAt(i));
if (type == 512 /* NSM */)
type = prev;
else if (type == 8 /* EN */ && prevStrong == 4 /* AL */)
type = 16 /* AN */;
types[i] = type == 4 /* AL */ ? 2 /* R */ : type;
if (type & 7 /* Strong */)
prevStrong = type;
prev = type;
}
// W5. A sequence of European terminators adjacent to European
// numbers changes to all European numbers.
// W6. Otherwise, separators and terminators change to Other
// Neutral.
// W7. Search backwards from each instance of a European number
// until the first strong type (R, L, or sor) is found. If an L is
// found, then change the type of the European number to L.
// (Left after this: L, R, EN+AN, NI)
for (let i = 0, prev = outerType, prevStrong = outerType; i < len; i++) {
let type = types[i];
if (type == 128 /* CS */) {
if (i < len - 1 && prev == types[i + 1] && (prev & 24 /* Num */))
type = types[i] = prev;
else
types[i] = 256 /* NI */;
}
else if (type == 64 /* ET */) {
let end = i + 1;
while (end < len && types[end] == 64 /* ET */)
end++;
let replace = (i && prev == 8 /* EN */) || (end < len && types[end] == 8 /* EN */) ? (prevStrong == 1 /* L */ ? 1 /* L */ : 8 /* EN */) : 256 /* NI */;
for (let j = i; j < end; j++)
types[j] = replace;
i = end - 1;
}
else if (type == 8 /* EN */ && prevStrong == 1 /* L */) {
types[i] = 1 /* L */;
}
prev = type;
if (type & 7 /* Strong */)
prevStrong = type;
}
// N1. A sequence of neutrals takes the direction of the
// surrounding strong text if the text on both sides has the same
// direction. European and Arabic numbers act as if they were R in
// terms of their influence on neutrals. Start-of-level-run (sor)
// and end-of-level-run (eor) are used at level run boundaries.
// N2. Any remaining neutrals take the embedding direction.
// (Left after this: L, R, EN+AN)
for (let i = 0; i < len; i++) {
if (types[i] == 256 /* NI */) {
let end = i + 1;
while (end < len && types[end] == 256 /* NI */)
end++;
let beforeL = (i ? types[i - 1] : outerType) == 1 /* L */;
let afterL = (end < len ? types[end] : outerType) == 1 /* L */;
let replace = beforeL == afterL ? (beforeL ? 1 /* L */ : 2 /* R */) : outerType;
for (let j = i; j < end; j++)
types[j] = replace;
i = end - 1;
}
}
// Here we depart from the documented algorithm, in order to avoid
// building up an actual levels array. Since there are only three
// levels (0, 1, 2) in an implementation that doesn't take
// explicit embedding into account, we can build up the order on
// the fly, without following the level-based algorithm.
let order = [];
if (outerType == 1 /* L */) {
for (let i = 0; i < len;) {
let start = i, rtl = types[i++] != 1 /* L */;
while (i < len && rtl == (types[i] != 1 /* L */))
i++;
if (rtl) {
for (let j = i; j > start;) {
let end = j, l = types[--j] != 2 /* R */;
while (j > start && l == (types[j - 1] != 2 /* R */))
j--;
order.push(new BidiSpan(j, end, l ? 2 : 1));
}
}
else {
order.push(new BidiSpan(start, i, 0));
}
}
}
else {
for (let i = 0; i < len;) {
let start = i, rtl = types[i++] == 2 /* R */;
while (i < len && rtl == (types[i] == 2 /* R */))
i++;
order.push(new BidiSpan(start, i, rtl ? 1 : 2));
}
}
return order;
}
function trivialOrder(length) {
return [new BidiSpan(0, length, 0)];
}
let movedOver = "";
function moveVisually(line, order, dir, start, forward) {
var _a;
let startIndex = start.head - line.from, spanI = -1;
if (startIndex == 0) {
if (!forward || !line.length)
return null;
if (order[0].level != dir) {
startIndex = order[0].side(false, dir);
spanI = 0;
}
}
else if (startIndex == line.length) {
if (forward)
return null;
let last = order[order.length - 1];
if (last.level != dir) {
startIndex = last.side(true, dir);
spanI = order.length - 1;
}
}
if (spanI < 0)
spanI = BidiSpan.find(order, startIndex, (_a = start.bidiLevel) !== null && _a !== void 0 ? _a : -1, start.assoc);
let span = order[spanI];
// End of span. (But not end of line--that was checked for above.)
if (startIndex == span.side(forward, dir)) {
span = order[spanI += forward ? 1 : -1];
startIndex = span.side(!forward, dir);
}
let indexForward = forward == (span.dir == dir);
let nextIndex = line.findClusterBreak(startIndex, indexForward);
movedOver = line.slice(Math.min(startIndex, nextIndex), Math.max(startIndex, nextIndex));
if (nextIndex != span.side(forward, dir))
return EditorSelection.cursor(nextIndex + line.from, indexForward ? -1 : 1, span.level);
let nextSpan = spanI == (forward ? order.length - 1 : 0) ? null : order[spanI + (forward ? 1 : -1)];
if (!nextSpan && span.level != dir)
return EditorSelection.cursor(forward ? line.to : line.from, forward ? -1 : 1, dir);
if (nextSpan && nextSpan.level < span.level)
return EditorSelection.cursor(nextSpan.side(!forward, dir) + line.from, 0, nextSpan.level);
return EditorSelection.cursor(nextIndex + line.from, 0, span.level);
}
const wrappingWhiteSpace = ["pre-wrap", "normal", "pre-line"];
class HeightOracle {
constructor() {
this.doc = Text.empty;
this.lineWrapping = false;
this.direction = Direction.LTR;
this.heightSamples = {};
this.lineHeight = 14;
this.charWidth = 7;
this.lineLength = 30;
// Used to track, during updateHeight, if any actual heights changed
this.heightChanged = false;
}
heightForGap(from, to) {
let lines = this.doc.lineAt(to).number - this.doc.lineAt(from).number + 1;
if (this.lineWrapping)
lines += Math.ceil(((to - from) - (lines * this.lineLength * 0.5)) / this.lineLength);
return this.lineHeight * lines;
}
heightForLine(length) {
if (!this.lineWrapping)
return this.lineHeight;
let lines = 1 + Math.max(0, Math.ceil((length - this.lineLength) / (this.lineLength - 5)));
return lines * this.lineHeight;
}
setDoc(doc) { this.doc = doc; return this; }
mustRefresh(lineHeights, whiteSpace, direction) {
let newHeight = false;
for (let i = 0; i < lineHeights.length; i++) {
let h = lineHeights[i];
if (h < 0) {
i++;
}
else if (!this.heightSamples[Math.floor(h * 10)]) { // Round to .1 pixels
newHeight = true;
this.heightSamples[Math.floor(h * 10)] = true;
}
}
return newHeight || (wrappingWhiteSpace.indexOf(whiteSpace) > -1) != this.lineWrapping || this.direction != direction;
}
refresh(whiteSpace, direction, lineHeight, charWidth, lineLength, knownHeights) {
let lineWrapping = wrappingWhiteSpace.indexOf(whiteSpace) > -1;
let changed = Math.round(lineHeight) != Math.round(this.lineHeight) ||
this.lineWrapping != lineWrapping ||
this.direction != direction;
this.lineWrapping = lineWrapping;
this.direction = direction;
this.lineHeight = lineHeight;
this.charWidth = charWidth;
this.lineLength = lineLength;
if (changed) {
this.heightSamples = {};
for (let i = 0; i < knownHeights.length; i++) {
let h = knownHeights[i];
if (h < 0)
i++;
else
this.heightSamples[Math.floor(h * 10)] = true;
}
}
return changed;
}
}
// This object is used by `updateHeight` to make DOM measurements
// arrive at the right nides. The `heights` array is a sequence of
// block heights, starting from position `from`.
class MeasuredHeights {
constructor(from, heights) {
this.from = from;
this.heights = heights;
this.index = 0;
}
get more() { return this.index < this.heights.length; }
}
/// Record used to represent information about a block-level element
/// in the editor view.
class BlockInfo {
/// @internal
constructor(
/// The start of the element in the document.
from,
/// The length of the element.
length,
/// The top position of the element.
top,
/// Its height.
height,
/// The type of element this is. When querying lines, this may be
/// an array of all the blocks that make up the line.
type) {
this.from = from;
this.length = length;
this.top = top;
this.height = height;
this.type = type;
}
/// The end of the element as a document position.
get to() { return this.from + this.length; }
/// The bottom position of the element.
get bottom() { return this.top + this.height; }
/// @internal
join(other) {
let detail = (Array.isArray(this.type) ? this.type : [this])
.concat(Array.isArray(other.type) ? other.type : [other]);
return new BlockInfo(this.from, this.length + other.length, this.top, this.height + other.height, detail);
}
}
var QueryType;
(function (QueryType) {
QueryType[QueryType["ByPos"] = 0] = "ByPos";
QueryType[QueryType["ByHeight"] = 1] = "ByHeight";
QueryType[QueryType["ByPosNoHeight"] = 2] = "ByPosNoHeight";
})(QueryType || (QueryType = {}));
const Epsilon = 1e-4;
class HeightMap {
constructor(length, // The number of characters covered
height, // Height of this part of the document
flags = 2 /* Outdated */) {
this.length = length;
this.height = height;
this.flags = flags;
}
get outdated() { return (this.flags & 2 /* Outdated */) > 0; }
set outdated(value) { this.flags = (value ? 2 /* Outdated */ : 0) | (this.flags & ~2 /* Outdated */); }
setHeight(oracle, height) {
if (this.height != height) {
if (Math.abs(this.height - height) > Epsilon)
oracle.heightChanged = true;
this.height = height;
}
}
// Base case is to replace a leaf node, which simply builds a tree
// from the new nodes and returns that (HeightMapBranch and
// HeightMapGap override this to actually use from/to)
replace(_from, _to, nodes) {
return HeightMap.of(nodes);
}
// Again, these are base cases, and are overridden for branch and gap nodes.
decomposeLeft(_to, result) { result.push(this); }
decomposeRight(_from, result) { result.push(this); }
applyChanges(decorations, oldDoc, oracle, changes) {
let me = this;
for (let i = changes.length - 1; i >= 0; i--) {
let { fromA, toA, fromB, toB } = changes[i];
let start = me.lineAt(fromA, QueryType.ByPosNoHeight, oldDoc, 0, 0);
let end = start.to >= toA ? start : me.lineAt(toA, QueryType.ByPosNoHeight, oldDoc, 0, 0);
toB += end.to - toA;
toA = end.to;
while (i > 0 && start.from <= changes[i - 1].toA) {
fromA = changes[i - 1].fromA;
fromB = changes[i - 1].fromB;
i--;
if (fromA < start.from)
start = me.lineAt(fromA, QueryType.ByPosNoHeight, oldDoc, 0, 0);
}
fromB += start.from - fromA;
fromA = start.from;
let nodes = NodeBuilder.build(oracle, decorations, fromB, toB);
me = me.replace(fromA, toA, nodes);
}
return me.updateHeight(oracle, 0);
}
static empty() { return new HeightMapText(0, 0); }
// nodes uses null values to indicate the position of line breaks.
// There are never line breaks at the start or end of the array, or
// two line breaks next to each other, and the array isn't allowed
// to be empty (same restrictions as return value from the builder).
static of(nodes) {
if (nodes.length == 1)
return nodes[0];
let i = 0, j = nodes.length, before = 0, after = 0;
for (;;) {
if (i == j) {
if (before > after * 2) {
let split = nodes[i - 1];
if (split.break)
nodes.splice(--i, 1, split.left, null, split.right);
else
nodes.splice(--i, 1, split.left, split.right);
j += 1 + split.break;
before -= split.size;
}
else if (after > before * 2) {
let split = nodes[j];
if (split.break)
nodes.splice(j, 1, split.left, null, split.right);
else
nodes.splice(j, 1, split.left, split.right);
j += 2 + split.break;
after -= split.size;
}
else {
break;
}
}
else if (before < after) {
let next = nodes[i++];
if (next)
before += next.size;
}
else {
let next = nodes[--j];
if (next)
after += next.size;
}
}
let brk = 0;
if (nodes[i - 1] == null) {
brk = 1;
i--;
}
else if (nodes[i] == null) {
brk = 1;
j++;
}
return new HeightMapBranch(HeightMap.of(nodes.slice(0, i)), brk, HeightMap.of(nodes.slice(j)));
}
}
HeightMap.prototype.size = 1;
class HeightMapBlock extends HeightMap {
constructor(length, height, type) {
super(length, height);
this.type = type;
}
blockAt(_height, _doc, top, offset) {
return new BlockInfo(offset, this.length, top, this.height, this.type);
}
lineAt(_value, _type, doc, top, offset) {
return this.blockAt(0, doc, top, offset);
}
forEachLine(_from, _to, doc, top, offset, f) {
f(this.blockAt(0, doc, top, offset));
}
updateHeight(oracle, offset = 0, _force = false, measured) {
if (measured && measured.from <= offset && measured.more)
this.setHeight(oracle, measured.heights[measured.index++]);
this.outdated = false;
return this;
}
toString() { return `block(${this.length})`; }
}
class HeightMapText extends HeightMapBlock {
constructor(length, height) {
super(length, height, BlockType.Text);
this.collapsed = 0; // Amount of collapsed content in the line
this.widgetHeight = 0; // Maximum inline widget height
}
replace(_from, _to, nodes) {
let node = nodes[0];
if (nodes.length == 1 && (node instanceof HeightMapText || node instanceof HeightMapGap && (node.flags & 4 /* SingleLine */)) &&
Math.abs(this.length - node.length) < 10) {
if (node instanceof HeightMapGap)
node = new HeightMapText(node.length, this.height);
else
node.height = this.height;
if (!this.outdated)
node.outdated = false;
return node;
}
else {
return HeightMap.of(nodes);
}
}
updateHeight(oracle, offset = 0, force = false, measured) {
if (measured && measured.from <= offset && measured.more)
this.setHeight(oracle, measured.heights[measured.index++]);
else if (force || this.outdated)
this.setHeight(oracle, Math.max(this.widgetHeight, oracle.heightForLine(this.length - this.collapsed)));
this.outdated = false;
return this;
}
toString() {
return `line(${this.length}${this.collapsed ? -this.collapsed : ""}${this.widgetHeight ? ":" + this.widgetHeight : ""})`;
}
}
class HeightMapGap extends HeightMap {
constructor(length) { super(length, 0); }
lines(doc, offset) {
let firstLine = doc.lineAt(offset).number, lastLine = doc.lineAt(offset + this.length).number;
return { firstLine, lastLine, lineHeight: this.height / (lastLine - firstLine + 1) };
}
blockAt(height, doc, top, offset) {
let { firstLine, lastLine, lineHeight } = this.lines(doc, offset);
let line = Math.max(0, Math.min(lastLine - firstLine, Math.floor((height - top) / lineHeight)));
let { from, length } = doc.line(firstLine + line);
return new BlockInfo(from, length, top + lineHeight * line, lineHeight, BlockType.Text);
}
lineAt(value, type, doc, top, offset) {
if (type == QueryType.ByHeight)
return this.blockAt(value, doc, top, offset);
if (type == QueryType.ByPosNoHeight) {
let { from, to } = doc.lineAt(value);
return new BlockInfo(from, to - from, 0, 0, BlockType.Text);
}
let { firstLine, lineHeight } = this.lines(doc, offset);
let { from, length, number } = doc.lineAt(value);
return new BlockInfo(from, length, top + lineHeight * (number - firstLine), lineHeight, BlockType.Text);
}
forEachLine(from, to, doc, top, offset, f) {
let { firstLine, lastLine, lineHeight } = this.lines(doc, offset);
for (let line = firstLine; line <= lastLine; line++) {
let { from, to } = doc.line(line);
if (from > to)
break;
if (to >= from)
f(new BlockInfo(from, to - from, top, top += lineHeight, BlockType.Text));
}
}
replace(from, to, nodes) {
let after = this.length - to;
if (after > 0) {
let last = nodes[nodes.length - 1];
if (last instanceof HeightMapGap)
nodes[nodes.length - 1] = new HeightMapGap(last.length + after);
else
nodes.push(null, new HeightMapGap(after - 1));
}
if (from > 0) {
let first = nodes[0];
if (first instanceof HeightMapGap)
nodes[0] = new HeightMapGap(from + first.length);
else
nodes.unshift(new HeightMapGap(from - 1), null);
}
return HeightMap.of(nodes);
}
decomposeLeft(to, result) {
result.push(new HeightMapGap(to - 1), null);
}
decomposeRight(from, result) {
result.push(null, new HeightMapGap(this.length - from - 1));
}
updateHeight(oracle, offset = 0, force = false, measured) {
let end = offset + this.length;
if (measured && measured.from <= offset + this.length && measured.more) {
// Fill in part of this gap with measured lines. We know there
// can't be widgets or collapsed ranges in those lines, because
// they would already have been added to the heightmap (gaps
// only contain plain text).
let nodes = [], pos = Math.max(offset, measured.from);
if (measured.from > offset)
nodes.push(new HeightMapGap(measured.from - offset - 1).updateHeight(oracle, offset));
while (pos <= end && measured.more) {
let len = oracle.doc.lineAt(pos).length;
if (nodes.length)
nodes.push(null);
let line = new HeightMapText(len, measured.heights[measured.index++]);
line.outdated = false;
nodes.push(line);
pos += len + 1;
}
if (pos <= end)
nodes.push(null, new HeightMapGap(end - pos).updateHeight(oracle, pos));
oracle.heightChanged = true;
return HeightMap.of(nodes);
}
else if (force || this.outdated) {
this.setHeight(oracle, oracle.heightForGap(offset, offset + this.length));
this.outdated = false;
}
return this;
}
toString() { return `gap(${this.length})`; }
}
class HeightMapBranch extends HeightMap {
constructor(left, brk, right) {
super(left.length + brk + right.length, left.height + right.height, brk | (left.outdated || right.outdated ? 2 /* Outdated */ : 0));
this.left = left;
this.right = right;
this.size = left.size + right.size;
}
get break() { return this.flags & 1 /* Break */; }
blockAt(height, doc, top, offset) {
let mid = top + this.left.height;
return height < mid || this.right.height == 0 ? this.left.blockAt(height, doc, top, offset)
: this.right.blockAt(height, doc, mid, offset + this.left.length + this.break);
}
lineAt(value, type, doc, top, offset) {
let rightTop = top + this.left.height, rightOffset = offset + this.left.length + this.break;
let left = type == QueryType.ByHeight ? value < rightTop || this.right.height == 0 : value < rightOffset;
let base = left ? this.left.lineAt(value, type, doc, top, offset)
: this.right.lineAt(value, type, doc, rightTop, rightOffset);
if (this.break || (left ? base.to < rightOffset : base.from > rightOffset))
return base;
let subQuery = type == QueryType.ByPosNoHeight ? QueryType.ByPosNoHeight : QueryType.ByPos;
if (left)
return base.join(this.right.lineAt(rightOffset, subQuery, doc, rightTop, rightOffset));
else
return this.left.lineAt(rightOffset, subQuery, doc, top, offset).join(base);
}
forEachLine(from, to, doc, top, offset, f) {
let rightTop = top + this.left.height, rightOffset = offset + this.left.length + this.break;
if (this.break) {
if (from < rightOffset)
this.left.forEachLine(from, to, doc, top, offset, f);
if (to >= rightOffset)
this.right.forEachLine(from, to, doc, rightTop, rightOffset, f);
}
else {
let mid = this.lineAt(rightOffset, QueryType.ByPos, doc, top, offset);
if (from < mid.from)
this.left.forEachLine(from, mid.from - 1, doc, top, offset, f);
if (mid.to >= from && mid.from <= to)
f(mid);
if (to > mid.to)
this.right.forEachLine(mid.to + 1, to, doc, rightTop, rightOffset, f);
}
}
replace(from, to, nodes) {
let rightStart = this.left.length + this.break;
if (to < rightStart)
return this.balanced(this.left.replace(from, to, nodes), this.right);
if (from > this.left.length)
return this.balanced(this.left, this.right.replace(from - rightStart, to - rightStart, nodes));
let result = [];
if (from > 0)
this.decomposeLeft(from, result);
let left = result.length;
for (let node of nodes)
result.push(node);
if (from > 0)
mergeGaps(result, left - 1);
if (to < this.length) {
let right = result.length;
this.decomposeRight(to, result);
mergeGaps(result, right);
}
return HeightMap.of(result);
}
decomposeLeft(to, result) {
let left = this.left.length;
if (to <= left)
return this.left.decomposeLeft(to, result);
result.push(this.left);
if (this.break) {
left++;
if (to >= left)
result.push(null);
}
if (to > left)
this.right.decomposeLeft(to - left, result);
}
decomposeRight(from, result) {
let left = this.left.length, right = left + this.break;
if (from >= right)
return this.right.decomposeRight(from - right, result);
if (from < left)
this.left.decomposeRight(from, result);
if (this.break && from < right)
result.push(null);
result.push(this.right);
}
balanced(left, right) {
if (left.size > 2 * right.size || right.size > 2 * left.size)
return HeightMap.of(this.break ? [left, null, right] : [left, right]);
this.left = left;
this.right = right;
this.height = left.height + right.height;
this.outdated = left.outdated || right.outdated;
this.size = left.size + right.size;
this.length = left.length + this.break + right.length;
return this;
}
updateHeight(oracle, offset = 0, force = false, measured) {
let { left, right } = this, rightStart = offset + left.length + this.break, rebalance = null;
if (measured && measured.from <= offset + left.length && measured.more)
rebalance = left = left.updateHeight(oracle, offset, force, measured);
else
left.updateHeight(oracle, offset, force);
if (measured && measured.from <= rightStart + right.length && measured.more)
rebalance = right = right.updateHeight(oracle, rightStart, force, measured);
else
right.updateHeight(oracle, rightStart, force);
if (rebalance)
return this.balanced(left, right);
this.height = this.left.height + this.right.height;
this.outdated = false;
return this;
}
toString() { return this.left + (this.break ? " " : "-") + this.right; }
}
function mergeGaps(nodes, around) {
let before, after;
if (nodes[around] == null &&
(before = nodes[around - 1]) instanceof HeightMapGap &&
(after = nodes[around + 1]) instanceof HeightMapGap)
nodes.splice(around - 1, 3, new HeightMapGap(before.length + 1 + after.length));
}
const relevantWidgetHeight = 5;
class NodeBuilder {
constructor(pos, oracle) {
this.pos = pos;
this.oracle = oracle;
this.nodes = [];
this.lineStart = -1;
this.lineEnd = -1;
this.covering = null;
this.writtenTo = pos;
}
get isCovered() {
return this.covering && this.nodes[this.nodes.length - 1] == this.covering;
}
span(_from, to) {
if (this.lineStart > -1) {
let end = Math.min(to, this.lineEnd), last = this.nodes[this.nodes.length - 1];
if (last instanceof HeightMapText)
last.length += end - this.pos;
else if (end > this.pos || !this.isCovered)
this.nodes.push(new HeightMapText(end - this.pos, -1));
this.writtenTo = end;
if (to > end) {
this.nodes.push(null);
this.writtenTo++;
this.lineStart = -1;
}
}
this.pos = to;
}
point(from, to, deco) {
if (from < to || deco.heightRelevant) {
let height = deco.widget ? Math.max(0, deco.widget.estimatedHeight) : 0;
let len = to - from;
if (deco.block) {
this.addBlock(new HeightMapBlock(len, height, deco.type));
}
else if (len || height >= relevantWidgetHeight) {
this.addLineDeco(height, len);
}
}
else if (to > from) {
this.span(from, to);
}
if (this.lineEnd > -1 && this.lineEnd < this.pos)
this.lineEnd = this.oracle.doc.lineAt(this.pos).to;
}
enterLine() {
if (this.lineStart > -1)
return;
let { from, to } = this.oracle.doc.lineAt(this.pos);
this.lineStart = from;
this.lineEnd = to;
if (this.writtenTo < from) {
if (this.writtenTo < from - 1 || this.nodes[this.nodes.length - 1] == null)
this.nodes.push(this.blankContent(this.writtenTo, from - 1));
this.nodes.push(null);
}
if (this.pos > from)
this.nodes.push(new HeightMapText(this.pos - from, -1));
this.writtenTo = this.pos;
}
blankContent(from, to) {
let gap = new HeightMapGap(to - from);
if (this.oracle.doc.lineAt(from).to == to)
gap.flags |= 4 /* SingleLine */;
return gap;
}
ensureLine() {
this.enterLine();
let last = this.nodes.length ? this.nodes[this.nodes.length - 1] : null;
if (last instanceof HeightMapText)
return last;
let line = new HeightMapText(0, -1);
this.nodes.push(line);
return line;
}
addBlock(block) {
this.enterLine();
if (block.type == BlockType.WidgetAfter && !this.isCovered)
this.ensureLine();
this.nodes.push(block);
this.writtenTo = this.pos = this.pos + block.length;
if (block.type != BlockType.WidgetBefore)
this.covering = block;
}
addLineDeco(height, length) {
let line = this.ensureLine();
line.length += length;
line.collapsed += length;
line.widgetHeight = Math.max(line.widgetHeight, height);
this.writtenTo = this.pos = this.pos + length;
}
finish(from) {
let last = this.nodes.length == 0 ? null : this.nodes[this.nodes.length - 1];
if (this.lineStart > -1 && !(last instanceof HeightMapText) && !this.isCovered)
this.nodes.push(new HeightMapText(0, -1));
else if (this.writtenTo < this.pos || last == null)
this.nodes.push(this.blankContent(this.writtenTo, this.pos));
let pos = from;
for (let node of this.nodes) {
if (node instanceof HeightMapText)
node.updateHeight(this.oracle, pos);
pos += node ? node.length : 1;
}
return this.nodes;
}
// Always called with a region that on both sides either stretches
// to a line break or the end of the document.
// The returned array uses null to indicate line breaks, but never
// starts or ends in a line break, or has multiple line breaks next
// to each other.
static build(oracle, decorations, from, to) {
let builder = new NodeBuilder(from, oracle);
RangeSet.spans(decorations, from, to, builder);
return builder.finish(from);
}
get minPointSize() { return 0; }
}
function heightRelevantDecoChanges(a, b, diff) {
let comp = new DecorationComparator();
RangeSet.compare(a, b, diff, comp);
return comp.changes;
}
class DecorationComparator {
constructor() {
this.changes = [];
}
compareRange() { }
comparePoint(from, to, a, b) {
if (from < to || a && a.heightRelevant || b && b.heightRelevant)
addRange(from, to, this.changes);
}
get minPointSize() { return 0; }
}
const none$3 = [];
const clickAddsSelectionRange = Facet.define();
const dragMovesSelection = Facet.define();
const mouseSelectionStyle = Facet.define();
const exceptionSink = Facet.define();
const updateListener = Facet.define();
/// Log or report an unhandled exception in client code. Should
/// probably only be used by extension code that allows client code to
/// provide functions, and calls those functions in a context where an
/// exception can't be propagated to calling code in a reasonable way
/// (for example when in an event handler).
///
/// Either calls a handler registered with
/// [`EditorView.exceptionSink`](#view.EditorView^exceptionSink),
/// `window.onerror`, if defined, or `console.error` (in which case
/// it'll pass `context`, when given, as first argument).
function logException(state, exception, context) {
let handler = state.facet(exceptionSink);
if (handler.length)
handler[0](exception);
else if (window.onerror)
window.onerror(String(exception), context, undefined, undefined, exception);
else if (context)
console.error(context + ":", exception);
else
console.error(exception);
}
const editable = Facet.define({ combine: values => values.length ? values[0] : true });
/// Plugin fields are a mechanism for allowing plugins to provide
/// values that can be retrieved through the
/// [`pluginField`](#view.EditorView.pluginField) view method.
class PluginField {
static define() { return new PluginField(); }
}
/// Plugins can provide additional scroll margins (space around the
/// sides of the scrolling element that should be considered
/// invisible) through this field. This can be useful when the
/// plugin introduces elements that cover part of that element (for
/// example a horizontally fixed gutter).
PluginField.scrollMargins = PluginField.define();
let nextPluginID = 0;
const viewPlugin = Facet.define();
/// View plugins associate stateful values with a view. They can
/// influence the way the content is drawn, and are notified of things
/// that happen in the view.
class ViewPlugin {
constructor(
/// @internal
id,
/// @internal
create,
/// @internal
fields) {
this.id = id;
this.create = create;
this.fields = fields;
this.extension = viewPlugin.of(this);
}
/// Define a plugin from a constructor function that creates the
/// plugin's value, given an editor view.
static define(create) {
return new ViewPlugin(nextPluginID++, create, []);
}
/// Create a plugin for a class whose constructor takes a single
/// editor view as argument.
static fromClass(cls) {
return ViewPlugin.define(view => new cls(view));
}
/// Create a new version of this plugin that provides a given
/// [plugin field](#view.PluginField).
provide(field, get) {
return new ViewPlugin(this.id, this.create, this.fields.concat({ field, get }));
}
decorations(get) {
return this.provide(pluginDecorations, get || ((value) => value.decorations));
}
/// Convenience method that extends a view plugin to automatically
/// register [DOM event
/// handlers](#view.EditorView^domEventHandlers).
eventHandlers(handlers) {
return this.provide(domEventHandlers, (value) => ({ plugin: value, handlers }));
}
}
// FIXME somehow ensure that no replacing decorations end up in here
const pluginDecorations = PluginField.define();
const domEventHandlers = PluginField.define();
class PluginInstance {
constructor(value, spec) {
this.value = value;
this.spec = spec;
this.updateFunc = this.value.update ? this.value.update.bind(this.value) : () => undefined;
}
static create(spec, view) {
let value;
try {
value = spec.create(view);
}
catch (e) {
logException(view.state, e, "CodeMirror plugin crashed");
return PluginInstance.dummy;
}
return new PluginInstance(value, spec);
}
takeField(type, target) {
for (let { field, get } of this.spec.fields)
if (field == type)
target.push(get(this.value));
}
update(update) {
try {
this.updateFunc(update);
return this;
}
catch (e) {
logException(update.state, e, "CodeMirror plugin crashed");
if (this.value.destroy)
try {
this.value.destroy();
}
catch (_) { }
return PluginInstance.dummy;
}
}
destroy(view) {
try {
if (this.value.destroy)
this.value.destroy();
}
catch (e) {
logException(view.state, e, "CodeMirror plugin crashed");
}
}
}
PluginInstance.dummy = new PluginInstance({}, ViewPlugin.define(() => ({})));
const editorAttributes = Facet.define({
combine: values => values.reduce((a, b) => combineAttrs(b, a), {})
});
const contentAttributes = Facet.define({
combine: values => values.reduce((a, b) => combineAttrs(b, a), {})
});
// Provide decorations
const decorations = Facet.define();
const styleModule = Facet.define();
class ChangedRange {
constructor(fromA, toA, fromB, toB) {
this.fromA = fromA;
this.toA = toA;
this.fromB = fromB;
this.toB = toB;
}
join(other) {
return new ChangedRange(Math.min(this.fromA, other.fromA), Math.max(this.toA, other.toA), Math.min(this.fromB, other.fromB), Math.max(this.toB, other.toB));
}
addToSet(set) {
let i = set.length, me = this;
for (; i > 0; i--) {
let range = set[i - 1];
if (range.fromA > me.toA)
continue;
if (range.toA < me.fromA)
break;
me = me.join(range);
set.splice(i - 1, 1);
}
set.splice(i, 0, me);
return set;
}
static extendWithRanges(diff, ranges) {
if (ranges.length == 0)
return diff;
let result = [];
for (let dI = 0, rI = 0, posA = 0, posB = 0;; dI++) {
let next = dI == diff.length ? null : diff[dI], off = posA - posB;
let end = next ? next.fromB : 1e9;
while (rI < ranges.length && ranges[rI] < end) {
let from = ranges[rI], to = ranges[rI + 1];
let fromB = Math.max(posB, from), toB = Math.min(end, to);
if (fromB <= toB)
new ChangedRange(fromB + off, toB + off, fromB, toB).addToSet(result);
if (to > end)
break;
else
rI += 2;
}
if (!next)
return result;
new ChangedRange(next.fromA, next.toA, next.fromB, next.toB).addToSet(result);
posA = next.toA;
posB = next.toB;
}
}
}
/// View [plugins](#view.ViewPlugin) are given instances of this
/// class, which describe what happened, whenever the view is updated.
class ViewUpdate {
/// @internal
constructor(
/// The editor view that the update is associated with.
view,
/// The new editor state.
state,
/// The transactions involved in the update. May be empty.
transactions = none$3) {
this.view = view;
this.state = state;
this.transactions = transactions;
/// @internal
this.flags = 0;
this.prevState = view.state;
this.changes = ChangeSet.empty(this.prevState.doc.length);
for (let tr of transactions)
this.changes = this.changes.compose(tr.changes);
let changedRanges = [];
this.changes.iterChangedRanges((fromA, toA, fromB, toB) => changedRanges.push(new ChangedRange(fromA, toA, fromB, toB)));
this.changedRanges = changedRanges;
let focus = view.hasFocus;
if (focus != view.inputState.notifiedFocused) {
view.inputState.notifiedFocused = focus;
this.flags != 1 /* Focus */;
}
if (this.docChanged)
this.flags |= 2 /* Height */;
}
/// Tells you whether the viewport changed in this update.
get viewportChanged() {
return (this.flags & 4 /* Viewport */) > 0;
}
/// Indicates whether the line height in the editor changed in this update.
get heightChanged() {
return (this.flags & 2 /* Height */) > 0;
}
/// True when this update indicates a focus change.
get focusChanged() {
return (this.flags & 1 /* Focus */) > 0;
}
/// Whether the document changed in this update.
get docChanged() {
return this.transactions.some(tr => tr.docChanged);
}
/// Whether the selection was explicitly set in this update.
get selectionSet() {
return this.transactions.some(tr => tr.selection);
}
/// @internal
get empty() { return this.flags == 0 && this.transactions.length == 0; }
}
function visiblePixelRange(dom, paddingTop) {
let rect = dom.getBoundingClientRect();
let left = Math.max(0, rect.left), right = Math.min(innerWidth, rect.right);
let top = Math.max(0, rect.top), bottom = Math.min(innerHeight, rect.bottom);
for (let parent = dom.parentNode; parent;) { // (Cast to any because TypeScript is useless with Node types)
if (parent.nodeType == 1) {
if ((parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth) &&
window.getComputedStyle(parent).overflow != "visible") {
let parentRect = parent.getBoundingClientRect();
left = Math.max(left, parentRect.left);
right = Math.min(right, parentRect.right);
top = Math.max(top, parentRect.top);
bottom = Math.min(bottom, parentRect.bottom);
}
parent = parent.parentNode;
}
else if (parent.nodeType == 11) { // Shadow root
parent = parent.host;
}
else {
break;
}
}
return { left: left - rect.left, right: right - rect.left,
top: top - (rect.top + paddingTop), bottom: bottom - (rect.top + paddingTop) };
}
// Line gaps are placeholder widgets used to hide pieces of overlong
// lines within the viewport, as a kludge to keep the editor
// responsive when a ridiculously long line is loaded into it.
class LineGap {
constructor(from, to, size) {
this.from = from;
this.to = to;
this.size = size;
}
static same(a, b) {
if (a.length != b.length)
return false;
for (let i = 0; i < a.length; i++) {
let gA = a[i], gB = b[i];
if (gA.from != gB.from || gA.to != gB.to || gA.size != gB.size)
return false;
}
return true;
}
draw(wrapping) {
return Decoration.replace({ widget: new LineGapWidget({ size: this.size, vertical: wrapping }) }).range(this.from, this.to);
}
}
class LineGapWidget extends WidgetType {
toDOM() {
let elt = document.createElement("div");
if (this.value.vertical) {
elt.style.height = this.value.size + "px";
}
else {
elt.style.width = this.value.size + "px";
elt.style.height = "2px";
elt.style.display = "inline-block";
}
return elt;
}
eq(other) { return this.value.size == other.size && this.value.vertical == other.vertical; }
get estimatedHeight() { return this.value.vertical ? this.value.size : -1; }
}
class ViewState {
constructor(state) {
this.state = state;
// These are contentDOM-local coordinates
this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
this.paddingTop = 0;
this.paddingBottom = 0;
this.heightOracle = new HeightOracle;
this.heightMap = HeightMap.empty();
this.scrollTo = null;
// Briefly set to true when printing, to disable viewport limiting
this.printing = false;
this.visibleRanges = [];
// Cursor 'assoc' is only significant when the cursor is on a line
// wrap point, where it must stick to the character that it is
// associated with. Since browsers don't provide a reasonable
// interface to set or query this, when a selection is set that
// might cause this to be signficant, this flag is set. The next
// measure phase will check whether the cursor is on a line-wrapping
// boundary and, if so, reset it to make sure it is positioned in
// the right place.
this.mustEnforceCursorAssoc = false;
this.heightMap = this.heightMap.applyChanges(state.facet(decorations), Text.empty, this.heightOracle.setDoc(state.doc), [new ChangedRange(0, 0, 0, state.doc.length)]);
this.viewport = this.getViewport(0, null);
this.lineGaps = this.ensureLineGaps([]);
this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(false)));
this.computeVisibleRanges();
}
update(update, scrollTo = null) {
let prev = this.state;
this.state = update.state;
let newDeco = this.state.facet(decorations);
let contentChanges = update.changedRanges;
let heightChanges = ChangedRange.extendWithRanges(contentChanges, heightRelevantDecoChanges(update.prevState.facet(decorations), newDeco, update ? update.changes : ChangeSet.empty(this.state.doc.length)));
let prevHeight = this.heightMap.height;
this.heightMap = this.heightMap.applyChanges(newDeco, prev.doc, this.heightOracle.setDoc(this.state.doc), heightChanges);
if (this.heightMap.height != prevHeight)
update.flags |= 2 /* Height */;
let viewport = heightChanges.length ? this.mapViewport(this.viewport, update.changes) : this.viewport;
if (scrollTo && (scrollTo.head < viewport.from || scrollTo.head > viewport.to) || !this.viewportIsAppropriate(viewport))
viewport = this.getViewport(0, scrollTo);
if (!viewport.eq(this.viewport)) {
this.viewport = viewport;
update.flags |= 4 /* Viewport */;
}
if (this.lineGaps.length || this.viewport.to - this.viewport.from > 15000 /* MinViewPort */)
update.flags |= this.updateLineGaps(this.ensureLineGaps(this.mapLineGaps(this.lineGaps, update.changes)));
this.computeVisibleRanges();
if (scrollTo)
this.scrollTo = scrollTo;
if (!this.mustEnforceCursorAssoc && update.selectionSet && update.view.lineWrapping &&
update.state.selection.primary.empty && update.state.selection.primary.assoc)
this.mustEnforceCursorAssoc = true;
}
measure(docView, repeated) {
let dom = docView.dom, whiteSpace = "", direction = Direction.LTR;
if (!repeated) {
// Vertical padding
let style = window.getComputedStyle(dom);
whiteSpace = style.whiteSpace, direction = (style.direction == "rtl" ? Direction.RTL : Direction.LTR);
this.paddingTop = parseInt(style.paddingTop) || 0;
this.paddingBottom = parseInt(style.paddingBottom) || 0;
}
// Pixel viewport
let pixelViewport = this.printing ? { top: -1e8, bottom: 1e8, left: -1e8, right: 1e8 } : visiblePixelRange(dom, this.paddingTop);
let dTop = pixelViewport.top - this.pixelViewport.top, dBottom = pixelViewport.bottom - this.pixelViewport.bottom;
this.pixelViewport = pixelViewport;
if (this.pixelViewport.bottom <= this.pixelViewport.top ||
this.pixelViewport.right <= this.pixelViewport.left)
return 0;
let lineHeights = docView.measureVisibleLineHeights();
let refresh = false, bias = 0;
if (!repeated) {
if (this.heightOracle.mustRefresh(lineHeights, whiteSpace, direction)) {
let { lineHeight, charWidth } = docView.measureTextSize();
refresh = this.heightOracle.refresh(whiteSpace, direction, lineHeight, charWidth, (docView.dom).clientWidth / charWidth, lineHeights);
if (refresh)
docView.minWidth = 0;
}
if (dTop > 0 && dBottom > 0)
bias = Math.max(dTop, dBottom);
else if (dTop < 0 && dBottom < 0)
bias = Math.min(dTop, dBottom);
}
this.heightOracle.heightChanged = false;
this.heightMap = this.heightMap.updateHeight(this.heightOracle, 0, refresh, new MeasuredHeights(this.viewport.from, lineHeights));
let result = this.heightOracle.heightChanged ? 2 /* Height */ : 0;
if (!this.viewportIsAppropriate(this.viewport, bias) ||
this.scrollTo && (this.scrollTo.head < this.viewport.from || this.scrollTo.head > this.viewport.to)) {
this.viewport = this.getViewport(bias, this.scrollTo);
result |= 4 /* Viewport */;
}
if (this.lineGaps.length || this.viewport.to - this.viewport.from > 15000 /* MinViewPort */)
result |= this.updateLineGaps(this.ensureLineGaps(refresh ? [] : this.lineGaps));
this.computeVisibleRanges();
if (this.mustEnforceCursorAssoc) {
this.mustEnforceCursorAssoc = false;
// This is done in the read stage, because moving the selection
// to a line end is going to trigger a layout anyway, so it
// can't be a pure write. It should be rare that it does any
// writing.
docView.enforceCursorAssoc();
}
return result;
}
getViewport(bias, scrollTo) {
// This will divide VP.Margin between the top and the
// bottom, depending on the bias (the change in viewport position
// since the last update). It'll hold a number between 0 and 1
let marginTop = 0.5 - Math.max(-0.5, Math.min(0.5, bias / 1000 /* Margin */ / 2));
let map = this.heightMap, doc = this.state.doc, { top, bottom } = this.pixelViewport;
let viewport = new Viewport(map.lineAt(top - marginTop * 1000 /* Margin */, QueryType.ByHeight, doc, 0, 0).from, map.lineAt(bottom + (1 - marginTop) * 1000 /* Margin */, QueryType.ByHeight, doc, 0, 0).to);
// If scrollTo is given, make sure the viewport includes that position
if (scrollTo) {
if (scrollTo.head < viewport.from) {
let { top: newTop } = map.lineAt(scrollTo.head, QueryType.ByPos, doc, 0, 0);
viewport = new Viewport(map.lineAt(newTop - 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).from, map.lineAt(newTop + (bottom - top) + 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).to);
}
else if (scrollTo.head > viewport.to) {
let { bottom: newBottom } = map.lineAt(scrollTo.head, QueryType.ByPos, doc, 0, 0);
viewport = new Viewport(map.lineAt(newBottom - (bottom - top) - 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).from, map.lineAt(newBottom + 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).to);
}
}
return viewport;
}
mapViewport(viewport, changes) {
let from = changes.mapPos(viewport.from, -1), to = changes.mapPos(viewport.to, 1);
return new Viewport(this.heightMap.lineAt(from, QueryType.ByPos, this.state.doc, 0, 0).from, this.heightMap.lineAt(to, QueryType.ByPos, this.state.doc, 0, 0).to);
}
// Checks if a given viewport covers the visible part of the
// document and not too much beyond that.
viewportIsAppropriate({ from, to }, bias = 0) {
let { top } = this.heightMap.lineAt(from, QueryType.ByPos, this.state.doc, 0, 0);
let { bottom } = this.heightMap.lineAt(to, QueryType.ByPos, this.state.doc, 0, 0);
return (from == 0 || top <= this.pixelViewport.top - Math.max(10 /* MinCoverMargin */, Math.min(-bias, 250 /* MaxCoverMargin */))) &&
(to == this.state.doc.length ||
bottom >= this.pixelViewport.bottom + Math.max(10 /* MinCoverMargin */, Math.min(bias, 250 /* MaxCoverMargin */))) &&
(top > this.pixelViewport.top - 2 * 1000 /* Margin */ && bottom < this.pixelViewport.bottom + 2 * 1000 /* Margin */);
}
mapLineGaps(gaps, changes) {
if (!gaps.length || changes.empty)
return gaps;
let mapped = [];
for (let gap of gaps)
if (!changes.touchesRange(gap.from, gap.to))
mapped.push(new LineGap(changes.mapPos(gap.from), changes.mapPos(gap.to), gap.size));
return mapped;
}
// Computes positions in the viewport where the start or end of a
// line should be hidden, trying to reuse existing line gaps when
// appropriate to avoid unneccesary redraws.
// Uses crude character-counting for the positioning and sizing,
// since actual DOM coordinates aren't always available and
// predictable. Relies on generous margins (see LG.Margin) to hide
// the artifacts this might produce from the user.
ensureLineGaps(current) {
let gaps = [];
// This won't work at all in predominantly right-to-left text.
if (this.heightOracle.direction != Direction.LTR)
return gaps;
this.heightMap.forEachLine(this.viewport.from, this.viewport.to, this.state.doc, 0, 0, line => {
if (line.length < 10000 /* Margin */)
return;
let structure = lineStructure(line.from, line.to, this.state);
if (structure.total < 10000 /* Margin */)
return;
let viewFrom, viewTo;
if (this.heightOracle.lineWrapping) {
if (line.from != this.viewport.from)
viewFrom = line.from;
else
viewFrom = findPosition(structure, (this.pixelViewport.top - line.top) / line.height);
if (line.to != this.viewport.to)
viewTo = line.to;
else
viewTo = findPosition(structure, (this.pixelViewport.bottom - line.top) / line.height);
}
else {
let totalWidth = structure.total * this.heightOracle.charWidth;
viewFrom = findPosition(structure, this.pixelViewport.left / totalWidth);
viewTo = findPosition(structure, this.pixelViewport.right / totalWidth);
}
let sel = this.state.selection.primary;
// Make sure the gap doesn't cover a selection end
if (sel.from <= viewFrom && sel.to >= line.from)
viewFrom = sel.from;
if (sel.from <= line.to && sel.to >= viewTo)
viewTo = sel.to;
let gapTo = viewFrom - 10000 /* Margin */, gapFrom = viewTo + 10000 /* Margin */;
if (gapTo > line.from + 5000 /* HalfMargin */)
gaps.push(find(current, gap => gap.from == line.from && gap.to > gapTo - 5000 /* HalfMargin */ && gap.to < gapTo + 5000 /* HalfMargin */) ||
new LineGap(line.from, gapTo, this.gapSize(line, gapTo, true, structure)));
if (gapFrom < line.to - 5000 /* HalfMargin */)
gaps.push(find(current, gap => gap.to == line.to && gap.from > gapFrom - 5000 /* HalfMargin */ &&
gap.from < gapFrom + 5000 /* HalfMargin */) ||
new LineGap(gapFrom, line.to, this.gapSize(line, gapFrom, false, structure)));
});
return gaps;
}
gapSize(line, pos, start, structure) {
if (this.heightOracle.lineWrapping) {
let height = line.height * findFraction(structure, pos);
return start ? height : line.height - height;
}
else {
let ratio = findFraction(structure, pos);
return structure.total * this.heightOracle.charWidth * (start ? ratio : 1 - ratio);
}
}
updateLineGaps(gaps) {
if (!LineGap.same(gaps, this.lineGaps)) {
this.lineGaps = gaps;
this.lineGapDeco = Decoration.set(gaps.map(gap => gap.draw(this.heightOracle.lineWrapping)));
return 16 /* LineGaps */;
}
return 0;
}
computeVisibleRanges() {
let deco = this.state.facet(decorations);
if (this.lineGaps.length)
deco = deco.concat(this.lineGapDeco);
let ranges = [];
RangeSet.spans(deco, this.viewport.from, this.viewport.to, {
span(from, to) { ranges.push({ from, to }); },
point() { },
minPointSize: 20
});
this.visibleRanges = ranges;
}
lineAt(pos, editorTop) {
return this.heightMap.lineAt(pos, QueryType.ByPos, this.state.doc, editorTop + this.paddingTop, 0);
}
lineAtHeight(height, editorTop) {
return this.heightMap.lineAt(height, QueryType.ByHeight, this.state.doc, editorTop + this.paddingTop, 0);
}
blockAtHeight(height, editorTop) {
return this.heightMap.blockAt(height, this.state.doc, editorTop + this.paddingTop, 0);
}
forEachLine(from, to, f, editorTop) {
return this.heightMap.forEachLine(from, to, this.state.doc, editorTop + this.paddingTop, 0, f);
}
}
/// Indicates the range of the document that is in the visible
/// viewport.
class Viewport {
constructor(from, to) {
this.from = from;
this.to = to;
}
eq(b) { return this.from == b.from && this.to == b.to; }
}
function lineStructure(from, to, state) {
let ranges = [], pos = from, total = 0;
RangeSet.spans(state.facet(decorations), from, to, {
span() { },
point(from, to) {
if (from > pos) {
ranges.push({ from: pos, to: from });
total += from - pos;
}
pos = to;
},
minPointSize: 20 // We're only interested in collapsed ranges of a significant size
});
if (pos < to) {
ranges.push({ from: pos, to });
total += to - pos;
}
return { total, ranges };
}
function findPosition({ total, ranges }, ratio) {
if (ratio <= 0)
return ranges[0].from;
if (ratio >= 1)
return ranges[ranges.length - 1].to;
let dist = Math.floor(total * ratio);
for (let i = 0;; i++) {
let { from, to } = ranges[i], size = to - from;
if (dist <= size)
return from + dist;
dist -= size;
}
}
function findFraction(structure, pos) {
let counted = 0;
for (let { from, to } of structure.ranges) {
if (pos <= to) {
counted += pos - from;
break;
}
counted += to - from;
}
return counted / structure.total;
}
function find(array, f) {
for (let val of array)
if (f(val))
return val;
return undefined;
}
const none$4 = [];
class DocView extends ContentView {
constructor(view) {
super();
this.view = view;
this.viewports = none$4;
this.compositionDeco = Decoration.none;
this.decorations = [];
// Track a minimum width for the editor. When measuring sizes in
// checkLayout, this is updated to point at the width of a given
// element and its extent in the document. When a change happens in
// that range, these are reset. That way, once we've seen a
// line/element of a given length, we keep the editor wide enough to
// fit at least that element, until it is changed, at which point we
// forget it again.
this.minWidth = 0;
this.minWidthFrom = 0;
this.minWidthTo = 0;
// Track whether the DOM selection was set in a lossy way, so that
// we don't mess it up when reading it back it
this.impreciseAnchor = null;
this.impreciseHead = null;
this.setDOM(view.contentDOM);
this.children = [new LineView];
this.children[0].setParent(this);
this.updateInner([new ChangedRange(0, 0, 0, view.state.doc.length)], this.updateDeco(), 0);
}
get root() { return this.view.root; }
get editorView() { return this.view; }
get length() { return this.view.state.doc.length; }
// Update the document view to a given state. scrollIntoView can be
// used as a hint to compute a new viewport that includes that
// position, if we know the editor is going to scroll that position
// into view.
update(update) {
var _a;
let changedRanges = update.changedRanges;
if (this.minWidth > 0 && changedRanges.length) {
if (!changedRanges.every(({ fromA, toA }) => toA < this.minWidthFrom || fromA > this.minWidthTo)) {
this.minWidth = 0;
}
else {
this.minWidthFrom = update.changes.mapPos(this.minWidthFrom, 1);
this.minWidthTo = update.changes.mapPos(this.minWidthTo, 1);
}
}
if (!((_a = this.view.inputState) === null || _a === void 0 ? void 0 : _a.composing))
this.compositionDeco = Decoration.none;
else if (update.transactions.length)
this.compositionDeco = computeCompositionDeco(this.view, update.changes);
// When the DOM nodes around the selection are moved to another
// parent, Chrome sometimes reports a different selection through
// getSelection than the one that it actually shows to the user.
// This forces a selection update when lines are joined to work
// around that. Issue #54
let forceSelection = (browser.ie || browser.chrome) && !this.compositionDeco.size && update &&
update.state.doc.lines != update.prevState.doc.lines;
let prevDeco = this.decorations, deco = this.updateDeco();
let decoDiff = findChangedDeco(prevDeco, deco, update.changes);
changedRanges = ChangedRange.extendWithRanges(changedRanges, decoDiff);
let pointerSel = update.transactions.some(tr => tr.annotation(Transaction.userEvent) == "pointerselection");
if (this.dirty == 0 /* Not */ && changedRanges.length == 0 &&
!(update.flags & (4 /* Viewport */ | 16 /* LineGaps */)) &&
update.state.selection.primary.from >= this.view.viewport.from &&
update.state.selection.primary.to <= this.view.viewport.to) {
this.updateSelection(forceSelection, pointerSel);
return false;
}
else {
this.updateInner(changedRanges, deco, update.prevState.doc.length, forceSelection, pointerSel);
return true;
}
}
// Used both by update and checkLayout do perform the actual DOM
// update
updateInner(changes, deco, oldLength, forceSelection = false, pointerSel = false) {
this.updateChildren(changes, deco, oldLength);
this.view.observer.ignore(() => {
// Lock the height during redrawing, since Chrome sometimes
// messes with the scroll position during DOM mutation (though
// no relayout is triggered and I cannot imagine how it can
// recompute the scroll position without a layout)
this.dom.style.height = this.view.viewState.heightMap.height + "px";
this.dom.style.minWidth = this.minWidth ? this.minWidth + "px" : "";
// Chrome will sometimes, when DOM mutations occur directly
// around the selection, get confused and report a different
// selection from the one it displays (issue #218). This tries
// to detect that situation.
let track = browser.chrome ? { node: getSelection(this.view.root).focusNode, written: false } : undefined;
this.sync(track);
this.dirty = 0 /* Not */;
if (track === null || track === void 0 ? void 0 : track.written)
forceSelection = true;
this.updateSelection(forceSelection, pointerSel);
this.dom.style.height = "";
});
}
updateChildren(changes, deco, oldLength) {
let cursor = this.childCursor(oldLength);
for (let i = changes.length - 1;; i--) {
let next = i >= 0 ? changes[i] : null;
if (!next)
break;
let { fromA, toA, fromB, toB } = next;
let { content, breakAtStart } = ContentBuilder.build(this.view.state.doc, fromB, toB, deco);
let { i: toI, off: toOff } = cursor.findPos(toA, 1);
let { i: fromI, off: fromOff } = cursor.findPos(fromA, -1);
this.replaceRange(fromI, fromOff, toI, toOff, content, breakAtStart);
}
}
replaceRange(fromI, fromOff, toI, toOff, content, breakAtStart) {
let before = this.children[fromI], last = content.length ? content[content.length - 1] : null;
let breakAtEnd = last ? last.breakAfter : breakAtStart;
// Change within a single line
if (fromI == toI && !breakAtStart && !breakAtEnd && content.length < 2 &&
before.merge(fromOff, toOff, content.length ? last : null, fromOff == 0))
return;
let after = this.children[toI];
// Make sure the end of the line after the update is preserved in `after`
if (toOff < after.length || after.children.length && after.children[after.children.length - 1].length == 0) {
// If we're splitting a line, separate part of the start line to
// avoid that being mangled when updating the start line.
if (fromI == toI) {
after = after.split(toOff);
toOff = 0;
}
// If the element after the replacement should be merged with
// the last replacing element, update `content`
if (!breakAtEnd && last && after.merge(0, toOff, last, true)) {
content[content.length - 1] = after;
}
else {
// Remove the start of the after element, if necessary, and
// add it to `content`.
if (toOff || after.children.length && after.children[0].length == 0)
after.merge(0, toOff, null, false);
content.push(after);
}
}
else if (after.breakAfter) {
// The element at `toI` is entirely covered by this range.
// Preserve its line break, if any.
if (last)
last.breakAfter = 1;
else
breakAtStart = 1;
}
// Since we've handled the next element from the current elements
// now, make sure `toI` points after that.
toI++;
before.breakAfter = breakAtStart;
if (fromOff > 0) {
if (!breakAtStart && content.length && before.merge(fromOff, before.length, content[0], false)) {
before.breakAfter = content.shift().breakAfter;
}
else if (fromOff < before.length || before.children.length && before.children[before.children.length - 1].length == 0) {
before.merge(fromOff, before.length, null, false);
}
fromI++;
}
// Try to merge widgets on the boundaries of the replacement
while (fromI < toI && content.length) {
if (this.children[toI - 1].match(content[content.length - 1]))
toI--, content.pop();
else if (this.children[fromI].match(content[0]))
fromI++, content.shift();
else
break;
}
if (fromI < toI || content.length)
this.replaceChildren(fromI, toI, content);
}
// Sync the DOM selection to this.state.selection
updateSelection(force = false, fromPointer = false) {
if (!(fromPointer || this.mayControlSelection()))
return;
let primary = this.view.state.selection.primary;
// FIXME need to handle the case where the selection falls inside a block range
let anchor = this.domAtPos(primary.anchor);
let head = this.domAtPos(primary.head);
let domSel = getSelection(this.root);
// If the selection is already here, or in an equivalent position, don't touch it
if (force || !domSel.focusNode ||
(browser.gecko && primary.empty && nextToUneditable(domSel.focusNode, domSel.focusOffset)) ||
!isEquivalentPosition(anchor.node, anchor.offset, domSel.anchorNode, domSel.anchorOffset) ||
!isEquivalentPosition(head.node, head.offset, domSel.focusNode, domSel.focusOffset)) {
this.view.observer.ignore(() => {
if (primary.empty) {
// Work around https://bugzilla.mozilla.org/show_bug.cgi?id=1612076
if (browser.gecko) {
let nextTo = nextToUneditable(anchor.node, anchor.offset);
if (nextTo && nextTo != (1 /* Before */ | 2 /* After */)) {
let text = nearbyTextNode(anchor.node, anchor.offset, nextTo == 1 /* Before */ ? 1 : -1);
if (text)
anchor = new DOMPos(text, nextTo == 1 /* Before */ ? 0 : text.nodeValue.length);
}
}
domSel.collapse(anchor.node, anchor.offset);
if (primary.bidiLevel != null && domSel.cursorBidiLevel != null)
domSel.cursorBidiLevel = primary.bidiLevel;
}
else if (domSel.extend) {
// Selection.extend can be used to create an 'inverted' selection
// (one where the focus is before the anchor), but not all
// browsers support it yet.
domSel.collapse(anchor.node, anchor.offset);
domSel.extend(head.node, head.offset);
}
else {
// Primitive (IE) way
let range = document.createRange();
if (primary.anchor > primary.head)
[anchor, head] = [head, anchor];
range.setEnd(head.node, head.offset);
range.setStart(anchor.node, anchor.offset);
domSel.removeAllRanges();
domSel.addRange(range);
}
});
}
this.impreciseAnchor = anchor.precise ? null : new DOMPos(domSel.anchorNode, domSel.anchorOffset);
this.impreciseHead = head.precise ? null : new DOMPos(domSel.focusNode, domSel.focusOffset);
}
enforceCursorAssoc() {
let cursor = this.view.state.selection.primary;
let sel = getSelection(this.root);
if (!cursor.empty || !cursor.assoc || !sel.modify)
return;
let line = LineView.find(this, cursor.head); // FIXME provide view-line-range finding helper
if (!line)
return;
let lineStart = line.posAtStart;
if (cursor.head == lineStart || cursor.head == lineStart + line.length)
return;
let before = this.coordsAt(cursor.head, -1), after = this.coordsAt(cursor.head, 1);
if (!before || !after || before.bottom > after.top)
return;
let dom = this.domAtPos(cursor.head + cursor.assoc);
sel.collapse(dom.node, dom.offset);
sel.modify("move", cursor.assoc < 0 ? "forward" : "backward", "lineboundary");
}
mayControlSelection() {
return this.view.state.facet(editable) ? this.root.activeElement == this.dom : hasSelection(this.dom, getSelection(this.root));
}
nearest(dom) {
for (let cur = dom; cur;) {
let domView = ContentView.get(cur);
if (domView && domView.rootView == this)
return domView;
cur = cur.parentNode;
}
return null;
}
posFromDOM(node, offset) {
let view = this.nearest(node);
if (!view)
throw new RangeError("Trying to find position for a DOM position outside of the document");
return view.localPosFromDOM(node, offset) + view.posAtStart;
}
domAtPos(pos) {
let { i, off } = this.childCursor().findPos(pos, -1);
for (; i < this.children.length - 1;) {
let child = this.children[i];
if (off < child.length || child instanceof LineView)
break;
i++;
off = 0;
}
return this.children[i].domAtPos(off);
}
coordsAt(pos, side) {
for (let off = this.length, i = this.children.length - 1;; i--) {
let child = this.children[i], start = off - child.breakAfter - child.length;
if (pos >= start && child.type != BlockType.WidgetAfter)
return child.coordsAt(pos - start, side);
off = start;
}
}
measureVisibleLineHeights() {
let result = [], { from, to } = this.view.viewState.viewport;
let minWidth = Math.max(this.view.scrollDOM.clientWidth, this.minWidth) + 1;
for (let pos = 0, i = 0; i < this.children.length; i++) {
let child = this.children[i], end = pos + child.length;
if (end > to)
break;
if (pos >= from) {
result.push(child.dom.getBoundingClientRect().height);
let width = child.dom.scrollWidth;
if (width > minWidth) {
this.minWidth = minWidth = width;
this.minWidthFrom = pos;
this.minWidthTo = end;
}
}
pos = end + child.breakAfter;
}
return result;
}
measureTextSize() {
for (let child of this.children) {
if (child instanceof LineView) {
let measure = child.measureTextSize();
if (measure)
return measure;
}
}
// If no workable line exists, force a layout of a measurable element
let dummy = document.createElement("div"), lineHeight, charWidth;
dummy.className = "cm-line";
dummy.textContent = "abc def ghi jkl mno pqr stu";
this.view.observer.ignore(() => {
this.dom.appendChild(dummy);
let rect = clientRectsFor(dummy.firstChild)[0];
lineHeight = dummy.getBoundingClientRect().height;
charWidth = rect ? rect.width / 27 : 7;
dummy.remove();
});
return { lineHeight, charWidth };
}
childCursor(pos = this.length) {
// Move back to start of last element when possible, so that
// `ChildCursor.findPos` doesn't have to deal with the edge case
// of being after the last element.
let i = this.children.length;
if (i)
pos -= this.children[--i].length;
return new ChildCursor(this.children, pos, i);
}
computeBlockGapDeco() {
let visible = this.view.viewState.viewport, viewports = [visible];
let { head, anchor } = this.view.state.selection.primary;
if (head < visible.from || head > visible.to) {
let { from, to } = this.view.viewState.lineAt(head, 0);
viewports.push(new Viewport(from, to));
}
if (!viewports.some(({ from, to }) => anchor >= from && anchor <= to)) {
let { from, to } = this.view.viewState.lineAt(anchor, 0);
viewports.push(new Viewport(from, to));
}
this.viewports = viewports.sort((a, b) => a.from - b.from);
let deco = [];
for (let pos = 0, i = 0;; i++) {
let next = i == viewports.length ? null : viewports[i];
let end = next ? next.from - 1 : this.length;
if (end > pos) {
let height = this.view.viewState.lineAt(end, 0).bottom - this.view.viewState.lineAt(pos, 0).top;
deco.push(Decoration.replace({ widget: new BlockGapWidget(height), block: true, inclusive: true }).range(pos, end));
}
if (!next)
break;
pos = next.to + 1;
}
return Decoration.set(deco);
}
updateDeco() {
return this.decorations = [
...this.view.state.facet(decorations),
this.computeBlockGapDeco(),
this.view.viewState.lineGapDeco,
this.compositionDeco,
...this.view.pluginField(pluginDecorations)
];
}
scrollPosIntoView(pos, side) {
let rect = this.coordsAt(pos, side);
if (!rect)
return;
let mLeft = 0, mRight = 0, mTop = 0, mBottom = 0;
for (let margins of this.view.pluginField(PluginField.scrollMargins))
if (margins) {
let { left, right, top, bottom } = margins;
if (left != null)
mLeft = Math.max(mLeft, left);
if (right != null)
mRight = Math.max(mRight, right);
if (top != null)
mTop = Math.max(mTop, top);
if (bottom != null)
mBottom = Math.max(mBottom, bottom);
}
scrollRectIntoView(this.dom, {
left: rect.left - mLeft, top: rect.top - mTop,
right: rect.right + mRight, bottom: rect.bottom + mBottom
});
}
}
// Browsers appear to reserve a fixed amount of bits for height
// styles, and ignore or clip heights above that. For Chrome and
// Firefox, this is in the 20 million range, so we try to stay below
// that.
const MaxNodeHeight = 1e7;
class BlockGapWidget extends WidgetType {
toDOM() {
let elt = document.createElement("div");
this.updateDOM(elt);
return elt;
}
updateDOM(elt) {
if (this.value < MaxNodeHeight) {
while (elt.lastChild)
elt.lastChild.remove();
elt.style.height = this.value + "px";
}
else {
elt.style.height = "";
for (let remaining = this.value; remaining > 0; remaining -= MaxNodeHeight) {
let fill = elt.appendChild(document.createElement("div"));
fill.style.height = Math.min(remaining, MaxNodeHeight) + "px";
}
}
return true;
}
get estimatedHeight() { return this.value; }
}
function computeCompositionDeco(view, changes) {
let sel = getSelection(view.root);
let textNode = sel.focusNode && nearbyTextNode(sel.focusNode, sel.focusOffset, 0);
if (!textNode)
return Decoration.none;
let cView = view.docView.nearest(textNode);
let from, to, topNode = textNode;
if (cView instanceof InlineView) {
from = cView.posAtStart;
to = from + cView.length;
topNode = cView.dom;
}
else if (cView instanceof LineView) {
while (topNode.parentNode != cView.dom)
topNode = topNode.parentNode;
let prev = topNode.previousSibling;
while (prev && !ContentView.get(prev))
prev = prev.previousSibling;
from = to = prev ? ContentView.get(prev).posAtEnd : cView.posAtStart;
}
else {
return Decoration.none;
}
let newFrom = changes.mapPos(from, 1), newTo = Math.max(newFrom, changes.mapPos(to, -1));
let text = textNode.nodeValue, { state } = view;
if (newTo - newFrom < text.length) {
if (state.sliceDoc(newFrom, Math.min(state.doc.length, newFrom + text.length)) == text)
newTo = newFrom + text.length;
else if (state.sliceDoc(Math.max(0, newTo - text.length), newTo) == text)
newFrom = newTo - text.length;
else
return Decoration.none;
}
else if (state.sliceDoc(newFrom, newTo) != text) {
return Decoration.none;
}
return Decoration.set(Decoration.replace({ widget: new CompositionWidget({ top: topNode, text: textNode }) }).range(newFrom, newTo));
}
class CompositionWidget extends WidgetType {
eq(value) { return this.value.top == value.top && this.value.text == value.text; }
toDOM() { return this.value.top; }
ignoreEvent() { return false; }
get customView() { return CompositionView; }
}
function nearbyTextNode(node, offset, side) {
for (;;) {
if (node.nodeType == 3)
return node;
if (node.nodeType == 1 && offset > 0 && side <= 0) {
node = node.childNodes[offset - 1];
offset = maxOffset(node);
}
else if (node.nodeType == 1 && offset < node.childNodes.length && side >= 0) {
node = node.childNodes[offset];
offset = 0;
}
else {
return null;
}
}
}
function nextToUneditable(node, offset) {
if (node.nodeType != 1)
return 0;
return (offset && node.childNodes[offset - 1].contentEditable == "false" ? 1 /* Before */ : 0) |
(offset < node.childNodes.length && node.childNodes[offset].contentEditable == "false" ? 2 /* After */ : 0);
}
class DecorationComparator$1 {
constructor() {
this.changes = [];
}
compareRange(from, to) { addRange(from, to, this.changes); }
comparePoint(from, to) { addRange(from, to, this.changes); }
}
function findChangedDeco(a, b, diff) {
let comp = new DecorationComparator$1;
RangeSet.compare(a, b, diff, comp);
return comp.changes;
}
function groupAt(state, pos, bias = 1) {
let categorize = state.charCategorizer(pos);
let line = state.doc.lineAt(pos), linePos = pos - line.from;
if (line.length == 0)
return EditorSelection.cursor(pos);
if (linePos == 0)
bias = 1;
else if (linePos == line.length)
bias = -1;
let from = linePos, to = linePos;
if (bias < 0)
from = line.findClusterBreak(linePos, false);
else
to = line.findClusterBreak(linePos, true);
let cat = categorize(line.slice(from, to));
while (from > 0) {
let prev = line.findClusterBreak(from, false);
if (categorize(line.slice(prev, from)) != cat)
break;
from = prev;
}
while (to < line.length) {
let next = line.findClusterBreak(to, true);
if (categorize(line.slice(to, next)) != cat)
break;
to = next;
}
return EditorSelection.range(from + line.from, to + line.from);
}
// Search the DOM for the {node, offset} position closest to the given
// coordinates. Very inefficient and crude, but can usually be avoided
// by calling caret(Position|Range)FromPoint instead.
// FIXME holding arrow-up/down at the end of the viewport is a rather
// common use case that will repeatedly trigger this code. Maybe
// introduce some element of binary search after all?
function getdx(x, rect) {
return rect.left > x ? rect.left - x : Math.max(0, x - rect.right);
}
function getdy(y, rect) {
return rect.top > y ? rect.top - y : Math.max(0, y - rect.bottom);
}
function yOverlap(a, b) {
return a.top < b.bottom - 1 && a.bottom > b.top + 1;
}
function upTop(rect, top) {
return top < rect.top ? { top, left: rect.left, right: rect.right, bottom: rect.bottom } : rect;
}
function upBot(rect, bottom) {
return bottom > rect.bottom ? { top: rect.top, left: rect.left, right: rect.right, bottom } : rect;
}
function domPosAtCoords(parent, x, y) {
let closest, closestRect, closestX, closestY;
let above, below, aboveRect, belowRect;
for (let child = parent.firstChild; child; child = child.nextSibling) {
let rects = clientRectsFor(child);
for (let i = 0; i < rects.length; i++) {
let rect = rects[i];
if (closestRect && yOverlap(closestRect, rect))
rect = upTop(upBot(rect, closestRect.bottom), closestRect.top);
let dx = getdx(x, rect), dy = getdy(y, rect);
if (dx == 0 && dy == 0)
return child.nodeType == 3 ? domPosInText(child, x, y) : domPosAtCoords(child, x, y);
if (!closest || closestY > dy || closestY == dy && closestX > dx) {
closest = child;
closestRect = rect;
closestX = dx;
closestY = dy;
}
if (dx == 0) {
if (y > rect.bottom && (!aboveRect || aboveRect.bottom < rect.bottom)) {
above = child;
aboveRect = rect;
}
else if (y < rect.top && (!belowRect || belowRect.top > rect.top)) {
below = child;
belowRect = rect;
}
}
else if (aboveRect && yOverlap(aboveRect, rect)) {
aboveRect = upBot(aboveRect, rect.bottom);
}
else if (belowRect && yOverlap(belowRect, rect)) {
belowRect = upTop(belowRect, rect.top);
}
}
}
if (aboveRect && aboveRect.bottom >= y) {
closest = above;
closestRect = aboveRect;
}
else if (belowRect && belowRect.top <= y) {
closest = below;
closestRect = belowRect;
}
if (!closest)
return { node: parent, offset: 0 };
let clipX = Math.max(closestRect.left, Math.min(closestRect.right, x));
if (closest.nodeType == 3)
return domPosInText(closest, clipX, y);
if (!closestX && closest.contentEditable == "true")
return domPosAtCoords(closest, clipX, y);
let offset = Array.prototype.indexOf.call(parent.childNodes, closest) +
(x >= (closestRect.left + closestRect.right) / 2 ? 1 : 0);
return { node: parent, offset };
}
function domPosInText(node, x, y) {
let len = node.nodeValue.length, range = document.createRange();
for (let i = 0; i < len; i++) {
range.setEnd(node, i + 1);
range.setStart(node, i);
let rects = range.getClientRects();
for (let j = 0; j < rects.length; j++) {
let rect = rects[j];
if (rect.top == rect.bottom)
continue;
if (rect.left - 1 <= x && rect.right + 1 >= x &&
rect.top - 1 <= y && rect.bottom + 1 >= y) {
let right = x >= (rect.left + rect.right) / 2, after = right;
if (browser.chrome || browser.gecko) {
// Check for RTL on browsers that support getting client
// rects for empty ranges.
range.setEnd(node, i);
let rectBefore = range.getBoundingClientRect();
if (rectBefore.left == rect.right)
after = !right;
}
return { node, offset: i + (after ? 1 : 0) };
}
}
}
return { node, offset: 0 };
}
function posAtCoords(view, { x, y }, bias = -1) {
let content = view.contentDOM.getBoundingClientRect(), block;
let halfLine = view.defaultLineHeight / 2;
for (let bounced = false;;) {
block = view.blockAtHeight(y, content.top);
if (block.top > y || block.bottom < y) {
bias = block.top > y ? -1 : 1;
y = Math.min(block.bottom - halfLine, Math.max(block.top + halfLine, y));
if (bounced)
return -1;
else
bounced = true;
}
if (block.type == BlockType.Text)
break;
y = bias > 0 ? block.bottom + halfLine : block.top - halfLine;
}
let lineStart = block.from;
// If this is outside of the rendered viewport, we can't determine a position
if (lineStart < view.viewport.from)
return view.viewport.from == 0 ? 0 : -1;
if (lineStart > view.viewport.to)
return view.viewport.to == view.state.doc.length ? view.state.doc.length : -1;
// Clip x to the viewport sides
x = Math.max(content.left + 1, Math.min(content.right - 1, x));
let root = view.root, element = root.elementFromPoint(x, y);
// There's visible editor content under the point, so we can try
// using caret(Position|Range)FromPoint as a shortcut
let node, offset = -1;
if (element && view.contentDOM.contains(element) && !(view.docView.nearest(element) instanceof WidgetView)) {
if (root.caretPositionFromPoint) {
let pos = root.caretPositionFromPoint(x, y);
if (pos)
({ offsetNode: node, offset } = pos);
}
else if (root.caretRangeFromPoint) {
let range = root.caretRangeFromPoint(x, y);
if (range)
({ startContainer: node, startOffset: offset } = range);
}
}
// No luck, do our own (potentially expensive) search
if (!node) {
let line = LineView.find(view.docView, lineStart);
({ node, offset } = domPosAtCoords(line.dom, x, y));
}
return view.docView.posFromDOM(node, offset);
}
function moveToLineBoundary(view, start, forward, includeWrap) {
let line = view.state.doc.lineAt(start.head);
let coords = !includeWrap || !view.lineWrapping ? null
: view.coordsAtPos(start.assoc < 0 && start.head > line.from ? start.head - 1 : start.head);
if (coords) {
let editorRect = view.dom.getBoundingClientRect();
let pos = view.posAtCoords({ x: forward == (view.textDirection == Direction.LTR) ? editorRect.right - 1 : editorRect.left + 1,
y: (coords.top + coords.bottom) / 2 });
if (pos > -1)
return EditorSelection.cursor(pos, forward ? -1 : 1);
}
let lineView = LineView.find(view.docView, start.head);
let end = lineView ? (forward ? lineView.posAtEnd : lineView.posAtStart) : (forward ? line.to : line.from);
return EditorSelection.cursor(end, forward ? -1 : 1);
}
function moveByChar(view, start, forward, by) {
let line = view.state.doc.lineAt(start.head), spans = view.bidiSpans(line);
for (let cur = start, check = null;;) {
let next = moveVisually(line, spans, view.textDirection, cur, forward), char = movedOver;
if (!next) {
if (line.number == (forward ? view.state.doc.lines : 1))
return cur;
char = "\n";
line = view.state.doc.line(line.number + (forward ? 1 : -1));
spans = view.bidiSpans(line);
next = EditorSelection.cursor(forward ? line.from : line.to);
}
if (!check) {
if (!by)
return next;
check = by(char);
}
else if (!check(char)) {
return cur;
}
cur = next;
}
}
function byGroup(view, pos, start) {
let categorize = view.state.charCategorizer(pos);
let cat = categorize(start);
return (next) => {
let nextCat = categorize(next);
if (cat == CharCategory.Space)
cat = nextCat;
return cat == nextCat;
};
}
function moveVertically(view, start, forward, distance) {
var _a;
let startPos = start.head, dir = forward ? 1 : -1;
let startCoords = view.coordsAtPos(startPos);
if (startCoords) {
let rect = view.dom.getBoundingClientRect();
let goal = (_a = start.goalColumn) !== null && _a !== void 0 ? _a : startCoords.left - rect.left;
let resolvedGoal = rect.left + goal;
let dist = distance !== null && distance !== void 0 ? distance : 5;
for (let startY = dir < 0 ? startCoords.top : startCoords.bottom, extra = 0; extra < 50; extra += 10) {
let pos = posAtCoords(view, { x: resolvedGoal, y: startY + (dist + extra) * dir }, dir);
if (pos < 0)
break;
if (pos != startPos)
return EditorSelection.cursor(pos, undefined, undefined, goal);
}
}
// Outside of the drawn viewport, use a crude column-based approach
let { doc } = view.state, line = doc.lineAt(startPos), tabSize = view.state.tabSize;
let goal = start.goalColumn, goalCol = 0;
if (goal == null) {
for (const iter = doc.iterRange(line.from, startPos); !iter.next().done;)
goalCol = countColumn(iter.value, goalCol, tabSize);
goal = goalCol * view.defaultCharacterWidth;
}
else {
goalCol = Math.round(goal / view.defaultCharacterWidth);
}
if (dir < 0 && line.from == 0)
return EditorSelection.cursor(0, undefined, undefined, goal);
else if (dir > 0 && line.to == doc.length)
return EditorSelection.cursor(line.to, undefined, undefined, goal);
let otherLine = doc.line(line.number + dir);
let result = otherLine.from;
let seen = 0;
for (const iter = doc.iterRange(otherLine.from, otherLine.to); seen >= goalCol && !iter.next().done;) {
const { offset, leftOver } = findColumn(iter.value, seen, goalCol, tabSize);
seen = goalCol - leftOver;
result += offset;
}
return EditorSelection.cursor(result, undefined, undefined, goal);
}
// This will also be where dragging info and such goes
class InputState {
constructor(view) {
this.lastKeyCode = 0;
this.lastKeyTime = 0;
this.lastSelectionOrigin = null;
this.lastSelectionTime = 0;
this.registeredEvents = [];
this.customHandlers = [];
this.composing = false;
this.compositionEndedAt = 0;
this.mouseSelection = null;
for (let type in handlers) {
let handler = handlers[type];
view.contentDOM.addEventListener(type, (event) => {
if (!eventBelongsToEditor(view, event) || this.ignoreDuringComposition(event))
return;
if (this.mustFlushObserver(event))
view.observer.forceFlush();
if (this.runCustomHandlers(type, view, event))
event.preventDefault();
else
handler(view, event);
});
this.registeredEvents.push(type);
}
// Must always run, even if a custom handler handled the event
view.contentDOM.addEventListener("keydown", (event) => {
view.inputState.lastKeyCode = event.keyCode;
view.inputState.lastKeyTime = Date.now();
});
if (view.root.activeElement == view.contentDOM)
view.dom.classList.add("cm-focused");
this.notifiedFocused = view.hasFocus;
this.ensureHandlers(view);
}
setSelectionOrigin(origin) {
this.lastSelectionOrigin = origin;
this.lastSelectionTime = Date.now();
}
ensureHandlers(view) {
let handlers = this.customHandlers = view.pluginField(domEventHandlers);
for (let set of handlers) {
for (let type in set.handlers)
if (this.registeredEvents.indexOf(type) < 0) {
this.registeredEvents.push(type);
(type != "scroll" ? view.contentDOM : view.scrollDOM).addEventListener(type, (event) => {
if (!eventBelongsToEditor(view, event))
return;
if (this.runCustomHandlers(type, view, event))
event.preventDefault();
});
}
}
}
runCustomHandlers(type, view, event) {
for (let set of this.customHandlers) {
let handler = set.handlers[type];
if (handler) {
try {
if (handler.call(set.plugin, event, view) || event.defaultPrevented)
return true;
}
catch (e) {
logException(view.state, e);
}
}
}
return false;
}
ignoreDuringComposition(event) {
if (!/^key/.test(event.type))
return false;
if (this.composing)
return true;
// See https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/.
// On some input method editors (IMEs), the Enter key is used to
// confirm character selection. On Safari, when Enter is pressed,
// compositionend and keydown events are sometimes emitted in the
// wrong order. The key event should still be ignored, even when
// it happens after the compositionend event.
if (browser.safari && event.timeStamp - this.compositionEndedAt < 500) {
this.compositionEndedAt = 0;
return true;
}
return false;
}
mustFlushObserver(event) {
return event.type == "keydown" || event.type == "compositionend";
}
startMouseSelection(view, event, style) {
if (this.mouseSelection)
this.mouseSelection.destroy();
this.mouseSelection = new MouseSelection(this, view, event, style);
}
update(update) {
if (this.mouseSelection)
this.mouseSelection.update(update);
this.lastKeyCode = this.lastSelectionTime = 0;
}
destroy() {
if (this.mouseSelection)
this.mouseSelection.destroy();
}
}
class MouseSelection {
constructor(inputState, view, startEvent, style) {
this.inputState = inputState;
this.view = view;
this.startEvent = startEvent;
this.style = style;
let doc = view.contentDOM.ownerDocument;
doc.addEventListener("mousemove", this.move = this.move.bind(this));
doc.addEventListener("mouseup", this.up = this.up.bind(this));
this.extend = startEvent.shiftKey;
this.multiple = view.state.facet(EditorState.allowMultipleSelections) && addsSelectionRange(view, startEvent);
this.dragMove = dragMovesSelection$1(view, startEvent);
this.dragging = isInPrimarySelection(view, startEvent) ? null : false;
// When clicking outside of the selection, immediately apply the
// effect of starting the selection
if (this.dragging === false) {
startEvent.preventDefault();
this.select(startEvent);
}
}
move(event) {
if (event.buttons == 0)
return this.destroy();
if (this.dragging !== false)
return;
this.select(event);
}
up() {
if (this.dragging == null)
this.select(this.startEvent);
this.destroy();
}
destroy() {
let doc = this.view.contentDOM.ownerDocument;
doc.removeEventListener("mousemove", this.move);
doc.removeEventListener("mouseup", this.up);
this.inputState.mouseSelection = null;
}
select(event) {
let selection = this.style.get(event, this.extend, this.multiple);
if (!selection.eq(this.view.state.selection) || selection.primary.assoc != this.view.state.selection.primary.assoc)
this.view.dispatch({
selection,
annotations: Transaction.userEvent.of("pointerselection"),
scrollIntoView: true
});
}
update(update) {
if (update.docChanged && this.dragging)
this.dragging = this.dragging.map(update.changes);
this.style.update(update);
}
}
function addsSelectionRange(view, event) {
let facet = view.state.facet(clickAddsSelectionRange);
return facet.length ? facet[0](event) : browser.mac ? event.metaKey : event.ctrlKey;
}
function dragMovesSelection$1(view, event) {
let facet = view.state.facet(dragMovesSelection);
return facet.length ? facet[0](event) : browser.mac ? !event.altKey : !event.ctrlKey;
}
function isInPrimarySelection(view, event) {
let { primary } = view.state.selection;
if (primary.empty)
return false;
// On boundary clicks, check whether the coordinates are inside the
// selection's client rectangles
let sel = getSelection(view.root);
if (sel.rangeCount == 0)
return true;
let rects = sel.getRangeAt(0).getClientRects();
for (let i = 0; i < rects.length; i++) {
let rect = rects[i];
if (rect.left <= event.clientX && rect.right >= event.clientX &&
rect.top <= event.clientY && rect.bottom >= event.clientY)
return true;
}
return false;
}
function eventBelongsToEditor(view, event) {
if (!event.bubbles)
return true;
if (event.defaultPrevented)
return false;
for (let node = event.target, cView; node != view.contentDOM; node = node.parentNode)
if (!node || node.nodeType == 11 || ((cView = ContentView.get(node)) && cView.ignoreEvent(event)))
return false;
return true;
}
const handlers = Object.create(null);
// This is very crude, but unfortunately both these browsers _pretend_
// that they have a clipboard API—all the objects and methods are
// there, they just don't work, and they are hard to test.
const brokenClipboardAPI = (browser.ie && browser.ie_version < 15) ||
(browser.ios && browser.webkit_version < 604);
function capturePaste(view) {
let parent = view.dom.parentNode;
if (!parent)
return;
let target = parent.appendChild(document.createElement("textarea"));
target.style.cssText = "position: fixed; left: -10000px; top: 10px";
target.focus();
setTimeout(() => {
view.focus();
target.remove();
doPaste(view, target.value);
}, 50);
}
function doPaste(view, input) {
let text = view.state.toText(input), i = 1;
let changes = text.lines == view.state.selection.ranges.length ?
view.state.changeByRange(range => {
let line = text.line(i++);
return { changes: { from: range.from, to: range.to, insert: line.slice() },
range: EditorSelection.cursor(range.from + line.length) };
}) : view.state.replaceSelection(text);
view.dispatch(changes, {
annotations: Transaction.userEvent.of("paste"),
scrollIntoView: true
});
}
function mustCapture(event) {
let mods = (event.ctrlKey ? 1 /* Ctrl */ : 0) | (event.metaKey ? 8 /* Meta */ : 0) |
(event.altKey ? 2 /* Alt */ : 0) | (event.shiftKey ? 4 /* Shift */ : 0);
let code = event.keyCode, macCtrl = browser.mac && mods == 1 /* Ctrl */;
return code == 8 || (macCtrl && code == 72) || // Backspace, Ctrl-h on Mac
code == 46 || (macCtrl && code == 68) || // Delete, Ctrl-d on Mac
code == 27 || // Esc
(mods == (browser.mac ? 8 /* Meta */ : 1 /* Ctrl */) && // Ctrl/Cmd-[biyz]
(code == 66 || code == 73 || code == 89 || code == 90));
}
handlers.keydown = (view, event) => {
if (mustCapture(event))
event.preventDefault();
view.inputState.setSelectionOrigin("keyboardselection");
};
handlers.touchdown = handlers.touchmove = view => {
view.inputState.setSelectionOrigin("pointerselection");
};
handlers.mousedown = (view, event) => {
let style = null;
for (let makeStyle of view.state.facet(mouseSelectionStyle)) {
style = makeStyle(view, event);
if (style)
break;
}
if (!style && event.button == 0)
style = basicMouseSelection(view, event);
if (style) {
if (view.root.activeElement != view.contentDOM)
view.observer.ignore(() => focusPreventScroll(view.contentDOM));
view.inputState.startMouseSelection(view, event, style);
}
};
function rangeForClick(view, pos, bias, type) {
if (type == 1) { // Single click
return EditorSelection.cursor(pos, bias);
}
else if (type == 2) { // Double click
return groupAt(view.state, pos, bias);
}
else { // Triple click
let line = LineView.find(view.docView, pos);
if (line)
return EditorSelection.range(line.posAtStart, line.posAtEnd);
let { from, to } = view.state.doc.lineAt(pos);
return EditorSelection.range(from, to);
}
}
let insideY = (y, rect) => y >= rect.top && y <= rect.bottom;
let inside = (x, y, rect) => insideY(y, rect) && x >= rect.left && x <= rect.right;
// Try to determine, for the given coordinates, associated with the
// given position, whether they are related to the element before or
// the element after the position.
function findPositionSide(view, pos, x, y) {
let line = LineView.find(view.docView, pos);
if (!line)
return 1;
let off = pos - line.posAtStart;
// Line boundaries point into the line
if (off == 0)
return 1;
if (off == line.length)
return -1;
// Positions on top of an element point at that element
let before = line.coordsAt(off, -1);
if (before && inside(x, y, before))
return -1;
let after = line.coordsAt(off, 1);
if (after && inside(x, y, after))
return 1;
// This is probably a line wrap point. Pick before if the point is
// beside it.
return before && insideY(y, before) ? -1 : 1;
}
function queryPos(view, event) {
let pos = view.posAtCoords({ x: event.clientX, y: event.clientY });
if (pos < 0)
return null;
return { pos, bias: findPositionSide(view, pos, event.clientX, event.clientY) };
}
const BadMouseDetail = browser.ie && browser.ie_version <= 11;
let lastMouseDown = null, lastMouseDownCount = 0;
function getClickType(event) {
if (!BadMouseDetail)
return event.detail;
let last = lastMouseDown;
lastMouseDown = event;
return lastMouseDownCount = !last || (last.timeStamp > Date.now() - 400 && Math.abs(last.clientX - event.clientX) < 2 &&
Math.abs(last.clientY - event.clientY) < 2) ? (lastMouseDownCount + 1) % 3 : 1;
}
function basicMouseSelection(view, event) {
let start = queryPos(view, event), type = getClickType(event);
let startSel = view.state.selection;
let last = start, lastEvent = event;
return {
update(update) {
if (update.changes) {
if (start)
start.pos = update.changes.mapPos(start.pos);
startSel = startSel.map(update.changes);
}
},
get(event, extend, multiple) {
let cur;
if (event.clientX == lastEvent.clientX && event.clientY == lastEvent.clientY)
cur = last;
else {
cur = last = queryPos(view, event);
lastEvent = event;
}
if (!cur || !start)
return startSel;
let range = rangeForClick(view, cur.pos, cur.bias, type);
if (start.pos != cur.pos && !extend) {
let startRange = rangeForClick(view, start.pos, start.bias, type);
let from = Math.min(startRange.from, range.from), to = Math.max(startRange.to, range.to);
range = from < range.from ? EditorSelection.range(from, to) : EditorSelection.range(to, from);
}
if (extend)
return startSel.replaceRange(startSel.primary.extend(range.from, range.to));
else if (multiple)
return startSel.addRange(range);
else
return EditorSelection.create([range]);
}
};
}
handlers.dragstart = (view, event) => {
let { selection: { primary } } = view.state;
let { mouseSelection } = view.inputState;
if (mouseSelection)
mouseSelection.dragging = primary;
if (event.dataTransfer) {
event.dataTransfer.setData("Text", view.state.sliceDoc(primary.from, primary.to));
event.dataTransfer.effectAllowed = "copyMove";
}
};
handlers.drop = (view, event) => {
if (!event.dataTransfer)
return;
let dropPos = view.posAtCoords({ x: event.clientX, y: event.clientY });
let text = event.dataTransfer.getData("Text");
if (dropPos < 0 || !text)
return;
event.preventDefault();
let { mouseSelection } = view.inputState;
let del = mouseSelection && mouseSelection.dragging && mouseSelection.dragMove ?
{ from: mouseSelection.dragging.from, to: mouseSelection.dragging.to } : null;
let ins = { from: dropPos, insert: text };
let changes = view.state.changes(del ? [del, ins] : ins);
view.focus();
view.dispatch({
changes,
selection: { anchor: changes.mapPos(dropPos, -1), head: changes.mapPos(dropPos, 1) },
annotations: Transaction.userEvent.of("drop")
});
};
handlers.paste = (view, event) => {
view.observer.flush();
let data = brokenClipboardAPI ? null : event.clipboardData;
let text = data && data.getData("text/plain");
if (text) {
doPaste(view, text);
event.preventDefault();
}
else {
capturePaste(view);
}
};
function captureCopy(view, text) {
// The extra wrapper is somehow necessary on IE/Edge to prevent the
// content from being mangled when it is put onto the clipboard
let parent = view.dom.parentNode;
if (!parent)
return;
let target = parent.appendChild(document.createElement("textarea"));
target.style.cssText = "position: fixed; left: -10000px; top: 10px";
target.value = text;
target.focus();
target.selectionEnd = text.length;
target.selectionStart = 0;
setTimeout(() => {
target.remove();
view.focus();
}, 50);
}
function copiedRange(state) {
let content = [], ranges = [];
for (let range of state.selection.ranges)
if (!range.empty) {
content.push(state.sliceDoc(range.from, range.to));
ranges.push(range);
}
if (!content.length) {
// Nothing selected, do a line-wise copy
let upto = -1;
for (let { from } of state.selection.ranges) {
let line = state.doc.lineAt(from);
if (line.number > upto) {
content.push(line.slice());
ranges.push({ from: line.from, to: Math.min(state.doc.length, line.to + 1) });
}
upto = line.number;
}
}
return { text: content.join(state.lineBreak), ranges };
}
handlers.copy = handlers.cut = (view, event) => {
let { text, ranges } = copiedRange(view.state);
if (!text)
return;
let data = brokenClipboardAPI ? null : event.clipboardData;
if (data) {
event.preventDefault();
data.clearData();
data.setData("text/plain", text);
}
else {
captureCopy(view, text);
}
if (event.type == "cut")
view.dispatch({
changes: ranges,
scrollIntoView: true,
annotations: Transaction.userEvent.of("cut")
});
};
handlers.focus = handlers.blur = view => {
setTimeout(() => {
if (view.hasFocus != view.inputState.notifiedFocused)
view.update([]);
}, 10);
};
handlers.beforeprint = view => {
view.viewState.printing = true;
view.requestMeasure();
setTimeout(() => {
view.viewState.printing = false;
view.requestMeasure();
}, 2000);
};
function forceClearComposition(view) {
if (view.docView.compositionDeco.size)
view.update([]);
}
handlers.compositionstart = handlers.compositionupdate = view => {
if (!view.inputState.composing) {
if (view.docView.compositionDeco.size) {
view.observer.flush();
forceClearComposition(view);
}
// FIXME possibly set a timeout to clear it again on Android
view.inputState.composing = true;
}
};
handlers.compositionend = view => {
view.inputState.composing = false;
view.inputState.compositionEndedAt = Date.now();
setTimeout(() => {
if (!view.inputState.composing)
forceClearComposition(view);
}, 50);
};
const observeOptions = {
childList: true,
characterData: true,
subtree: true,
characterDataOldValue: true
};
// IE11 has very broken mutation observers, so we also listen to
// DOMCharacterDataModified there
const useCharData = browser.ie && browser.ie_version <= 11;
class DOMObserver {
constructor(view, onChange, onScrollChanged) {
this.view = view;
this.onChange = onChange;
this.onScrollChanged = onScrollChanged;
this.active = false;
this.ignoreSelection = new DOMSelection;
this.delayedFlush = -1;
this.queue = [];
this.scrollTargets = [];
this.intersection = null;
this.intersecting = false;
// Timeout for scheduling check of the parents that need scroll handlers
this.parentCheck = -1;
this.dom = view.contentDOM;
this.observer = new MutationObserver(mutations => {
for (let mut of mutations)
this.queue.push(mut);
// IE11 will sometimes (on typing over a selection or
// backspacing out a single character text node) call the
// observer callback before actually updating the DOM
if (browser.ie && browser.ie_version <= 11 &&
mutations.some(m => m.type == "childList" && m.removedNodes.length ||
m.type == "characterData" && m.oldValue.length > m.target.nodeValue.length))
this.flushSoon();
else
this.flush();
});
if (useCharData)
this.onCharData = (event) => {
this.queue.push({ target: event.target,
type: "characterData",
oldValue: event.prevValue });
this.flushSoon();
};
this.onSelectionChange = () => {
if (this.view.root.activeElement != this.dom)
return;
// Deletions on IE11 fire their events in the wrong order, giving
// us a selection change event before the DOM changes are
// reported.
if (browser.ie && browser.ie_version <= 11 && !this.view.state.selection.primary.empty) {
let sel = getSelection(this.view.root);
// Selection.isCollapsed isn't reliable on IE
if (sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset))
return this.flushSoon();
}
this.flush();
};
this.start();
this.onScroll = this.onScroll.bind(this);
window.addEventListener("scroll", this.onScroll);
if (typeof IntersectionObserver == "function") {
this.intersection = new IntersectionObserver(entries => {
if (this.parentCheck < 0)
this.parentCheck = setTimeout(this.listenForScroll.bind(this), 1000);
if (entries[entries.length - 1].intersectionRatio > 0 != this.intersecting) {
this.intersecting = !this.intersecting;
this.onScroll();
}
}, {});
this.intersection.observe(this.dom);
}
this.listenForScroll();
}
onScroll() {
if (this.intersecting) {
this.flush();
this.onScrollChanged();
}
}
listenForScroll() {
this.parentCheck = -1;
let i = 0, changed = null;
for (let dom = this.dom; dom;) {
if (dom.nodeType == 1) {
if (!changed && i < this.scrollTargets.length && this.scrollTargets[i] == dom)
i++;
else if (!changed)
changed = this.scrollTargets.slice(0, i);
if (changed)
changed.push(dom);
dom = dom.parentNode;
}
else if (dom.nodeType == 11) { // Shadow root
dom = dom.host;
}
else {
break;
}
}
if (i < this.scrollTargets.length && !changed)
changed = this.scrollTargets.slice(0, i);
if (changed) {
for (let dom of this.scrollTargets)
dom.removeEventListener("scroll", this.onScroll);
for (let dom of this.scrollTargets = changed)
dom.addEventListener("scroll", this.onScroll);
}
}
ignore(f) {
if (!this.active)
return f();
try {
this.stop();
return f();
}
finally {
this.start();
this.clear();
}
}
start() {
if (this.active)
return;
this.observer.observe(this.dom, observeOptions);
// FIXME is this shadow-root safe?
this.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange);
if (useCharData)
this.dom.addEventListener("DOMCharacterDataModified", this.onCharData);
this.active = true;
}
stop() {
if (!this.active)
return;
this.active = false;
this.observer.disconnect();
this.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange);
if (useCharData)
this.dom.removeEventListener("DOMCharacterDataModified", this.onCharData);
}
clearSelection() {
this.ignoreSelection.set(getSelection(this.view.root));
}
// Throw away any pending changes
clear() {
this.observer.takeRecords();
this.queue.length = 0;
this.clearSelection();
}
flushSoon() {
if (this.delayedFlush < 0)
this.delayedFlush = window.setTimeout(() => { this.delayedFlush = -1; this.flush(); }, 20);
}
forceFlush() {
if (this.delayedFlush >= 0) {
window.clearTimeout(this.delayedFlush);
this.delayedFlush = -1;
this.flush();
}
}
// Apply pending changes, if any
flush() {
if (this.delayedFlush >= 0)
return;
let records = this.queue;
for (let mut of this.observer.takeRecords())
records.push(mut);
if (records.length)
this.queue = [];
let selection = getSelection(this.view.root);
let newSel = !this.ignoreSelection.eq(selection) && hasSelection(this.dom, selection);
if (records.length == 0 && !newSel)
return;
let from = -1, to = -1, typeOver = false;
for (let record of records) {
let range = this.readMutation(record);
if (!range)
continue;
if (range.typeOver)
typeOver = true;
if (from == -1) {
({ from, to } = range);
}
else {
from = Math.min(range.from, from);
to = Math.max(range.to, to);
}
}
let apply = from > -1 || newSel;
if (!apply || !this.onChange(from, to, typeOver)) {
if (this.view.docView.dirty) {
this.ignore(() => this.view.docView.sync());
this.view.docView.dirty = 0 /* Not */;
}
this.view.docView.updateSelection();
}
this.clearSelection();
}
readMutation(rec) {
let cView = this.view.docView.nearest(rec.target);
if (!cView || cView.ignoreMutation(rec))
return null;
cView.markDirty();
if (rec.type == "childList") {
let childBefore = findChild(cView, rec.previousSibling || rec.target.previousSibling, -1);
let childAfter = findChild(cView, rec.nextSibling || rec.target.nextSibling, 1);
return { from: childBefore ? cView.posAfter(childBefore) : cView.posAtStart,
to: childAfter ? cView.posBefore(childAfter) : cView.posAtEnd, typeOver: false };
}
else { // "characterData"
return { from: cView.posAtStart, to: cView.posAtEnd, typeOver: rec.target.nodeValue == rec.oldValue };
}
}
destroy() {
this.stop();
if (this.intersection)
this.intersection.disconnect();
for (let dom of this.scrollTargets)
dom.removeEventListener("scroll", this.onScroll);
window.removeEventListener("scroll", this.onScroll);
clearTimeout(this.parentCheck);
}
}
function findChild(cView, dom, dir) {
while (dom) {
let curView = ContentView.get(dom);
if (curView && curView.parent == cView)
return curView;
let parent = dom.parentNode;
dom = parent != cView.dom ? parent : dir > 0 ? dom.nextSibling : dom.previousSibling;
}
return null;
}
// FIXME reconsider this kludge (does it break reading dom text with newlines?)
const LineSep = "\ufdda"; // A Unicode 'non-character', used to denote newlines internally
function applyDOMChange(view, start, end, typeOver) {
let change, newSel;
let sel = view.state.selection.primary, bounds;
if (start > -1 && (bounds = view.docView.domBoundsAround(start, end, 0))) {
let { from, to } = bounds;
let selPoints = view.docView.impreciseHead || view.docView.impreciseAnchor ? [] : selectionPoints(view.contentDOM, view.root);
let reader = new DOMReader(selPoints);
reader.readRange(bounds.startDOM, bounds.endDOM);
newSel = selectionFromPoints(selPoints, from);
let preferredPos = sel.from, preferredSide = null;
// Prefer anchoring to end when Backspace is pressed
if (view.inputState.lastKeyCode === 8 && view.inputState.lastKeyTime > Date.now() - 100) {
preferredPos = sel.to;
preferredSide = "end";
}
let diff = findDiff(view.state.doc.sliceString(from, to, LineSep), reader.text, preferredPos - from, preferredSide);
if (diff)
change = { from: from + diff.from, to: from + diff.toA,
insert: Text$1.of(reader.text.slice(diff.from, diff.toB).split(LineSep)) };
}
else if (view.hasFocus) {
let domSel = getSelection(view.root);
let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ? view.state.selection.primary.head
: view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset
? view.state.selection.primary.anchor
: selectionCollapsed(domSel) ? head : view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
if (head != sel.head || anchor != sel.anchor)
newSel = EditorSelection.single(anchor, head);
}
if (!change && !newSel)
return false;
// Heuristic to notice typing over a selected character
if (!change && typeOver && !sel.empty && newSel && newSel.primary.empty)
change = { from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to) };
if (change) {
let startState = view.state;
// Android browsers don't fire reasonable key events for enter,
// backspace, or delete. So this detects changes that look like
// they're caused by those keys, and reinterprets them as key
// events.
if (browser.android &&
((change.from == sel.from && change.to == sel.to &&
change.insert.length == 1 && change.insert.lines == 2 &&
dispatchKey(view, "Enter", 10)) ||
(change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 &&
dispatchKey(view, "Backspace", 8)) ||
(change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
dispatchKey(view, "Delete", 46))))
return view.state != startState;
let tr;
if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3) {
let before = sel.from < change.from ? startState.doc.sliceString(sel.from, change.from, LineSep) : "";
let after = sel.to > change.to ? startState.doc.sliceString(change.to, sel.to, LineSep) : "";
tr = startState.replaceSelection(Text$1.of((before + change.insert.sliceString(0, undefined, LineSep) + after).split(LineSep)));
}
else {
let changes = startState.changes(change);
tr = {
changes,
selection: newSel && !startState.selection.primary.eq(newSel.primary) && newSel.primary.to <= changes.newLength
? startState.selection.replaceRange(newSel.primary) : undefined
};
}
view.dispatch(tr, { scrollIntoView: true, annotations: Transaction.userEvent.of("input") });
return true;
}
else if (newSel && !newSel.primary.eq(sel)) {
let scrollIntoView = false, annotations;
if (view.inputState.lastSelectionTime > Date.now() - 50) {
if (view.inputState.lastSelectionOrigin == "keyboardselection")
scrollIntoView = true;
else
annotations = Transaction.userEvent.of(view.inputState.lastSelectionOrigin);
}
view.dispatch({ selection: newSel, scrollIntoView, annotations });
return true;
}
return false;
}
function findDiff(a, b, preferredPos, preferredSide) {
let minLen = Math.min(a.length, b.length);
let from = 0;
while (from < minLen && a.charCodeAt(from) == b.charCodeAt(from))
from++;
if (from == minLen && a.length == b.length)
return null;
let toA = a.length, toB = b.length;
while (toA > 0 && toB > 0 && a.charCodeAt(toA - 1) == b.charCodeAt(toB - 1)) {
toA--;
toB--;
}
if (preferredSide == "end") {
let adjust = Math.max(0, from - Math.min(toA, toB));
preferredPos -= toA + adjust - from;
}
if (toA < from && a.length < b.length) {
let move = preferredPos <= from && preferredPos >= toA ? from - preferredPos : 0;
from -= move;
toB = from + (toB - toA);
toA = from;
}
else if (toB < from) {
let move = preferredPos <= from && preferredPos >= toB ? from - preferredPos : 0;
from -= move;
toA = from + (toA - toB);
toB = from;
}
return { from, toA, toB };
}
class DOMReader {
constructor(points) {
this.points = points;
this.text = "";
}
readRange(start, end) {
if (!start)
return;
let parent = start.parentNode;
for (let cur = start;;) {
this.findPointBefore(parent, cur);
this.readNode(cur);
let next = cur.nextSibling;
if (next == end)
break;
let view = ContentView.get(cur), nextView = ContentView.get(next);
if ((view ? view.breakAfter : isBlockElement(cur)) ||
((nextView ? nextView.breakAfter : isBlockElement(next)) && !(cur.nodeName == "BR" && !cur.cmIgnore)))
this.text += LineSep;
cur = next;
}
this.findPointBefore(parent, end);
}
readNode(node) {
if (node.cmIgnore)
return;
let view = ContentView.get(node);
let fromView = view && view.overrideDOMText;
let text;
if (fromView != null)
text = fromView.sliceString(0, undefined, LineSep);
else if (node.nodeType == 3)
text = node.nodeValue;
else if (node.nodeName == "BR")
text = node.nextSibling ? LineSep : "";
else if (node.nodeType == 1)
this.readRange(node.firstChild, null);
if (text != null) {
this.findPointIn(node, text.length);
this.text += text;
}
}
findPointBefore(node, next) {
for (let point of this.points)
if (point.node == node && node.childNodes[point.offset] == next)
point.pos = this.text.length;
}
findPointIn(node, maxLen) {
for (let point of this.points)
if (point.node == node)
point.pos = this.text.length + Math.min(point.offset, maxLen);
}
}
function isBlockElement(node) {
return node.nodeType == 1 && /^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(node.nodeName);
}
class DOMPoint {
constructor(node, offset) {
this.node = node;
this.offset = offset;
this.pos = -1;
}
}
function selectionPoints(dom, root) {
let result = [];
if (root.activeElement != dom)
return result;
let { anchorNode, anchorOffset, focusNode, focusOffset } = getSelection(root);
if (anchorNode) {
result.push(new DOMPoint(anchorNode, anchorOffset));
if (focusNode != anchorNode || focusOffset != anchorOffset)
result.push(new DOMPoint(focusNode, focusOffset));
}
return result;
}
function selectionFromPoints(points, base) {
if (points.length == 0)
return null;
let anchor = points[0].pos, head = points.length == 2 ? points[1].pos : anchor;
return anchor > -1 && head > -1 ? EditorSelection.single(anchor + base, head + base) : null;
}
function dispatchKey(view, name, code) {
let options = { key: name, code: name, keyCode: code, which: code, cancelable: true };
let down = new KeyboardEvent("keydown", options);
view.contentDOM.dispatchEvent(down);
let up = new KeyboardEvent("keyup", options);
view.contentDOM.dispatchEvent(up);
return down.defaultPrevented || up.defaultPrevented;
}
// The editor's update state machine looks something like this:
//
// Idle → Updating ⇆ Idle (unchecked) → Measuring → Idle
// ↑ ↓
// Updating (measure)
//
// The difference between 'Idle' and 'Idle (unchecked)' lies in
// whether a layout check has been scheduled. A regular update through
// the `update` method updates the DOM in a write-only fashion, and
// relies on a check (scheduled with `requestAnimationFrame`) to make
// sure everything is where it should be and the viewport covers the
// visible code. That check continues to measure and then optionally
// update until it reaches a coherent state.
/// An editor view represents the editor's user interface. It holds
/// the editable DOM surface, and possibly other elements such as the
/// line number gutter. It handles events and dispatches state
/// transactions for editing actions.
class EditorView {
/// Construct a new view. You'll usually want to put `view.dom` into
/// your document after creating a view, so that the user can see
/// it.
constructor(
/// Configuration options.
config = {}) {
this.plugins = [];
this.editorAttrs = {};
this.contentAttrs = {};
this.bidiCache = [];
/// @internal
this.updateState = 2 /* Updating */;
/// @internal
this.measureScheduled = -1;
/// @internal
this.measureRequests = [];
this.contentDOM = document.createElement("div");
this.scrollDOM = document.createElement("div");
this.scrollDOM.className = themeClass("scroller");
this.scrollDOM.appendChild(this.contentDOM);
this.dom = document.createElement("div");
this.dom.appendChild(this.scrollDOM);
this._dispatch = config.dispatch || ((tr) => this.update([tr]));
this.dispatch = this.dispatch.bind(this);
this.root = (config.root || document);
this.viewState = new ViewState(config.state || EditorState.create());
this.plugins = this.state.facet(viewPlugin).map(spec => PluginInstance.create(spec, this));
this.observer = new DOMObserver(this, (from, to, typeOver) => applyDOMChange(this, from, to, typeOver), () => this.measure());
this.docView = new DocView(this);
this.inputState = new InputState(this);
this.mountStyles();
this.updateAttrs();
this.updateState = 0 /* Idle */;
ensureGlobalHandler();
this.requestMeasure();
if (config.parent)
config.parent.appendChild(this.dom);
}
/// The current editor state.
get state() { return this.viewState.state; }
/// To be able to display large documents without consuming too much
/// memory or overloading the browser, CodeMirror only draws the
/// code that is visible (plus a margin around it) to the DOM. This
/// property tells you the extent of the current drawn viewport, in
/// document positions.
get viewport() { return this.viewState.viewport; }
/// When there are, for example, large collapsed ranges in the
/// viewport, its size can be a lot bigger than the actual visible
/// content. Thus, if you are doing something like styling the
/// content in the viewport, it is preferable to only do so for
/// these ranges, which are the subset of the viewport that is
/// actually drawn.
get visibleRanges() { return this.viewState.visibleRanges; }
dispatch(...input) {
this._dispatch(input.length == 1 && input[0] instanceof Transaction ? input[0]
: this.state.update(...input));
}
/// Update the view for the given array of transactions. This will
/// update the visible document and selection to match the state
/// produced by the transactions, and notify view plugins of the
/// change. You should usually call
/// [`dispatch`](#view.EditorView.dispatch) instead, which uses this
/// as a primitive.
update(transactions) {
if (this.updateState != 0 /* Idle */)
throw new Error("Calls to EditorView.update are not allowed while an update is in progress");
this.updateState = 2 /* Updating */;
let state = this.state;
for (let tr of transactions) {
if (tr.startState != state)
throw new RangeError("Trying to update state with a transaction that doesn't start from the previous state.");
state = tr.state;
}
let update = new ViewUpdate(this, state, transactions);
let scrollTo = transactions.some(tr => tr.scrolledIntoView) ? state.selection.primary : null;
this.viewState.update(update, scrollTo);
this.bidiCache = CachedOrder.update(this.bidiCache, update.changes);
if (!update.empty)
this.updatePlugins(update);
let redrawn = this.docView.update(update);
if (this.state.facet(styleModule) != this.styleModules)
this.mountStyles();
this.updateAttrs();
this.updateState = 0 /* Idle */;
if (redrawn || scrollTo || this.viewState.mustEnforceCursorAssoc)
this.requestMeasure();
for (let listener of this.state.facet(updateListener))
listener(update);
}
/// Reset the view to the given state. (This will cause the entire
/// document to be redrawn and all view plugins to be reinitialized,
/// so you should probably only use it when the new state isn't
/// derived from the old state. Otherwise, use
/// [`update`](#view.EditorView.update) instead.)
setState(newState) {
if (this.updateState != 0 /* Idle */)
throw new Error("Calls to EditorView.setState are not allowed while an update is in progress");
this.updateState = 2 /* Updating */;
for (let plugin of this.plugins)
plugin.destroy(this);
this.viewState = new ViewState(newState);
this.plugins = newState.facet(viewPlugin).map(spec => PluginInstance.create(spec, this));
this.docView = new DocView(this);
this.inputState.ensureHandlers(this);
this.mountStyles();
this.updateAttrs();
this.bidiCache = [];
this.updateState = 0 /* Idle */;
this.requestMeasure();
}
updatePlugins(update) {
let prevSpecs = update.prevState.facet(viewPlugin), specs = update.state.facet(viewPlugin);
if (prevSpecs != specs) {
let newPlugins = [], reused = [];
for (let spec of specs) {
let found = prevSpecs.indexOf(spec);
if (found < 0) {
newPlugins.push(PluginInstance.create(spec, this));
}
else {
let plugin = this.plugins[found].update(update);
reused.push(plugin);
newPlugins.push(plugin);
}
}
for (let plugin of this.plugins)
if (reused.indexOf(plugin) < 0)
plugin.destroy(this);
this.plugins = newPlugins;
this.inputState.ensureHandlers(this);
}
else {
for (let i = 0; i < this.plugins.length; i++)
this.plugins[i] = this.plugins[i].update(update);
}
}
/// @internal
measure() {
if (this.measureScheduled > -1)
cancelAnimationFrame(this.measureScheduled);
this.measureScheduled = 1; // Prevent requestMeasure calls from scheduling another animation frame
let updated = null;
for (let i = 0;; i++) {
this.updateState = 1 /* Measuring */;
let changed = this.viewState.measure(this.docView, i > 0);
let measuring = this.measureRequests;
if (!changed && !measuring.length && this.viewState.scrollTo == null)
break;
this.measureRequests = [];
if (i > 5) {
console.warn("Viewport failed to stabilize");
break;
}
let measured = measuring.map(m => {
try {
return m.read(this);
}
catch (e) {
logException(this.state, e);
return BadMeasure;
}
});
let update = new ViewUpdate(this, this.state);
update.flags |= changed;
if (!updated)
updated = update;
else
updated.flags |= changed;
this.updateState = 2 /* Updating */;
this.updatePlugins(update);
if (changed)
this.docView.update(update);
for (let i = 0; i < measuring.length; i++)
if (measured[i] != BadMeasure) {
try {
measuring[i].write(measured[i], this);
}
catch (e) {
logException(this.state, e);
}
}
if (this.viewState.scrollTo) {
this.docView.scrollPosIntoView(this.viewState.scrollTo.head, this.viewState.scrollTo.assoc);
this.viewState.scrollTo = null;
}
if (!(changed & 4 /* Viewport */) && this.measureRequests.length == 0)
break;
}
this.updateState = 0 /* Idle */;
this.measureScheduled = -1;
if (updated)
for (let listener of this.state.facet(updateListener))
listener(updated);
}
/// Get the CSS classes for the currently active editor themes.
get themeClasses() {
return baseThemeID + " " +
(this.state.facet(darkTheme) ? baseDarkThemeID : baseLightThemeID) + " " +
this.state.facet(theme);
}
updateAttrs() {
let editorAttrs = combineAttrs(this.state.facet(editorAttributes), {
class: themeClass("wrap") + (this.hasFocus ? " cm-focused " : " ") + this.themeClasses
});
updateAttrs(this.dom, this.editorAttrs, editorAttrs);
this.editorAttrs = editorAttrs;
let contentAttrs = combineAttrs(this.state.facet(contentAttributes), {
spellcheck: "false",
contenteditable: String(this.state.facet(editable)),
class: themeClass("content"),
style: `${browser.tabSize}: ${this.state.tabSize}`,
role: "textbox",
"aria-multiline": "true"
});
updateAttrs(this.contentDOM, this.contentAttrs, contentAttrs);
this.contentAttrs = contentAttrs;
}
mountStyles() {
this.styleModules = this.state.facet(styleModule);
StyleModule.mount(this.root, this.styleModules.concat(baseTheme).reverse());
}
/// Find the DOM parent node and offset (child offset if `node` is
/// an element, character offset when it is a text node) at the
/// given document position.
domAtPos(pos) {
return this.docView.domAtPos(pos);
}
/// Find the document position at the given DOM node. Can be useful
/// for associating positions with DOM events. Will raise an error
/// when `node` isn't part of the editor content.
posAtDOM(node, offset = 0) {
return this.docView.posFromDOM(node, offset);
}
readMeasured() {
if (this.updateState == 2 /* Updating */)
throw new Error("Reading the editor layout isn't allowed during an update");
if (this.updateState == 0 /* Idle */ && this.measureScheduled > -1)
this.measure();
}
/// Make sure plugins get a chance to measure the DOM before the
/// next frame. Calling this is preferable to messing with the DOM
/// directly from, for example, an even handler, because it'll make
/// sure measuring and drawing done by other components is
/// synchronized, avoiding unnecessary DOM layout computations.
requestMeasure(request) {
if (this.measureScheduled < 0)
this.measureScheduled = requestAnimationFrame(() => this.measure());
if (request) {
if (request.key != null)
for (let i = 0; i < this.measureRequests.length; i++) {
if (this.measureRequests[i].key === request.key) {
this.measureRequests[i] = request;
return;
}
}
this.measureRequests.push(request);
}
}
/// Collect all values provided by the active plugins for a given
/// field.
pluginField(field) {
// FIXME make this error when called during plugin updating
let result = [];
for (let plugin of this.plugins)
plugin.takeField(field, result);
return result;
}
/// Get the value of a specific plugin, if present. Note that
/// plugins that crash can be dropped from a view, so even when you
/// know you registered a given plugin, it is recommended to check
/// the return value of this method.
plugin(plugin) {
for (let inst of this.plugins)
if (inst.spec == plugin)
return inst.value;
return null;
}
/// Find the line or block widget at the given vertical position.
/// `editorTop`, if given, provides the vertical position of the top
/// of the editor. It defaults to the editor's screen position
/// (which will force a DOM layout).
blockAtHeight(height, editorTop) {
this.readMeasured();
return this.viewState.blockAtHeight(height, ensureTop(editorTop, this.contentDOM));
}
/// Find information for the visual line (see
/// [`visualLineAt`](#view.EditorView.visualLineAt)) at the given
/// vertical position. The resulting block info might hold another
/// array of block info structs in its `type` field if this line
/// consists of more than one block.
///
/// Heights are interpreted relative to the given `editorTop`
/// position. When not given, the top position of the editor's
/// [content element](#view.EditorView.contentDOM) is taken.
visualLineAtHeight(height, editorTop) {
this.readMeasured();
return this.viewState.lineAtHeight(height, ensureTop(editorTop, this.contentDOM));
}
/// Find the extent and height of the visual line (the content shown
/// in the editor as a line, which may be smaller than a document
/// line when broken up by block widgets, or bigger than a document
/// line when line breaks are covered by replaced decorations) at
/// the given position.
///
/// Vertical positions are computed relative to the `editorTop`
/// argument. You can pass `view.dom.getBoundingClientRect().top`
/// here to get screen coordinates.
visualLineAt(pos, editorTop = 0) {
return this.viewState.lineAt(pos, editorTop);
}
/// Iterate over the height information of the lines in the
/// viewport.
viewportLines(f, editorTop) {
let { from, to } = this.viewport;
this.viewState.forEachLine(from, to, f, ensureTop(editorTop, this.contentDOM));
}
/// The editor's total content height.
get contentHeight() {
return this.viewState.heightMap.height + this.viewState.paddingTop + this.viewState.paddingBottom;
}
/// Move a cursor position by [grapheme
/// cluster](#text.nextClusterBreak). `forward` determines whether
/// the motion is away from the line start, or towards it. Motion in
/// bidirectional text is in visual order, in the editor's [text
/// direction](#view.EditorView.textDirection). When the start
/// position was the last one on the line, the returned position
/// will be across the line break. If there is no further line, the
/// original position is returned.
moveByChar(start, forward, by) {
return moveByChar(this, start, forward, by);
}
/// Move a cursor position across the next group of either
/// [letters](#state.EditorState.charCategorizer) or non-letter
/// non-whitespace characters.
moveByGroup(start, forward) {
return moveByChar(this, start, forward, initial => byGroup(this, start.head, initial));
}
/// Move to the next line boundary in the given direction. If
/// `includeWrap` is true, line wrapping is on, and there is a
/// further wrap point on the current line, the wrap point will be
/// returned. Otherwise this function will return the start or end
/// of the line.
moveToLineBoundary(start, forward, includeWrap = true) {
return moveToLineBoundary(this, start, forward, includeWrap);
}
/// Move a cursor position vertically. When `distance` isn't given,
/// it defaults to moving to the next line (including wrapped
/// lines). Otherwise, `distance` should provide a positive distance
/// in pixels.
///
/// When `start` has a
/// [`goalColumn`](#state.SelectionRange.goalColumn), the vertical
/// motion will use that as a target horizontal position. Otherwise,
/// the cursor's own horizontal position is used. The returned
/// cursor will have its goal column set to whichever column was
/// used.
moveVertically(start, forward, distance) {
return moveVertically(this, start, forward, distance);
}
/// Scroll the given document position into view.
scrollPosIntoView(pos) {
this.viewState.scrollTo = EditorSelection.cursor(pos);
this.requestMeasure();
}
/// Get the document position at the given screen coordinates.
/// Returns -1 if no valid position could be found.
posAtCoords(coords) {
this.readMeasured();
// FIXME return null instead, so you at least get a type error
// when you forget the failure case?
return posAtCoords(this, coords);
}
/// Get the screen coordinates at the given document position.
coordsAtPos(pos, side = 1) {
this.readMeasured();
let line = this.state.doc.lineAt(pos), order = this.bidiSpans(line);
let rect = this.docView.coordsAt(pos, side);
if (!rect || rect.left == rect.right)
return rect;
let span = order[BidiSpan.find(order, pos - line.from, -1, side)];
return flattenRect(rect, (span.dir == Direction.LTR) == (side > 0));
}
/// The default width of a character in the editor. May not
/// accurately reflect the width of all characters.
get defaultCharacterWidth() { return this.viewState.heightOracle.charWidth; }
/// The default height of a line in the editor.
get defaultLineHeight() { return this.viewState.heightOracle.lineHeight; }
/// The text direction (`direction` CSS property) of the editor.
get textDirection() { return this.viewState.heightOracle.direction; }
/// Whether this editor [wraps lines](#view.EditorView.lineWrapping)
/// (as determined by the `white-space` CSS property of its content
/// element).
get lineWrapping() { return this.viewState.heightOracle.lineWrapping; }
/// Returns the bidirectional text structure of the given line
/// (which should be in the current document) as an array of span
/// objects. The order of these spans matches the [text
/// direction](#view.EditorView.textDirection)—if that is
/// left-to-right, the leftmost spans come first, otherwise the
/// rightmost spans come first.
bidiSpans(line) {
if (line.length > MaxBidiLine)
return trivialOrder(line.length);
let dir = this.textDirection;
for (let entry of this.bidiCache)
if (entry.from == line.from && entry.dir == dir)
return entry.order;
let order = computeOrder(line.slice(), this.textDirection);
this.bidiCache.push(new CachedOrder(line.from, line.to, dir, order));
return order;
}
/// Check whether the editor has focus.
get hasFocus() {
return this.root.activeElement == this.contentDOM;
}
/// Put focus on the editor.
focus() {
this.observer.ignore(() => {
focusPreventScroll(this.contentDOM);
this.docView.updateSelection();
});
}
/// Clean up this editor view, removing its element from the
/// document, unregistering event handlers, and notifying
/// plugins. The view instance can no longer be used after
/// calling this.
destroy() {
for (let plugin of this.plugins)
plugin.destroy(this);
this.inputState.destroy();
this.dom.remove();
this.observer.destroy();
if (this.measureScheduled > -1)
cancelAnimationFrame(this.measureScheduled);
}
/// Facet that can be used to add DOM event handlers. The value
/// should be an object mapping event names to handler functions. The
/// first such function to return true will be assumed to have handled
/// that event, and no other handlers or built-in behavior will be
/// activated for it.
static domEventHandlers(handlers) {
return ViewPlugin.define(() => ({})).eventHandlers(handlers);
}
/// Create a theme extension. The argument object should map [theme
/// selectors](#view.themeClass) to styles, which are (potentially
/// nested) [style
/// declarations](https://github.com/marijnh/style-mod#documentation)
/// providing the CSS styling for the selector.
///
/// When `dark` is set to true, the theme will be marked as dark,
/// which causes the [base theme](#view.EditorView^baseTheme) rules
/// marked with `@dark` to apply instead of those marked with
/// `@light`.
static theme(spec, options) {
let prefix = StyleModule.newName();
let result = [theme.of(prefix), styleModule.of(buildTheme(prefix, spec))];
if (options && options.dark)
result.push(darkTheme.of(true));
return result;
}
/// Create an extension that adds styles to the base theme. The
/// given object works much like the one passed to
/// [`theme`](#view.EditorView^theme), but allows selectors to be
/// marked by adding `@dark` to their end to only apply when there
/// is a dark theme active, or by `@light` to only apply when there
/// is _no_ dark theme active.
static baseTheme(spec) {
return precedence(styleModule.of(buildTheme(baseThemeID, spec)), "fallback");
}
}
/// Facet to add a [style
/// module](https://github.com/marijnh/style-mod#readme) to an editor
/// view. The view will ensure that the module is registered in its
/// [document root](#view.EditorView.constructor^config.root).
EditorView.styleModule = styleModule;
/// Allows you to provide a function that should be called when the
/// library catches an exception from an extension (mostly from view
/// plugins, but may be used by other extensions to route exceptions
/// from user-code-provided callbacks). This is mostly useful for
/// debugging and logging. See [`logException`](#view.logException).
EditorView.exceptionSink = exceptionSink;
/// A facet that can be used to have a listener function be notified
/// every time the view updates.
EditorView.updateListener = updateListener;
/// Facet that controls whether the editor content is editable. When
/// its the highest-precedence value is `false`, editing is
/// disabled, and the content element will no longer have its
/// `contenteditable` attribute set to `true`. (Note that this
/// doesn't affect API calls that change the editor content, even
/// when those are bound to keys or buttons.)
EditorView.editable = editable;
/// Facet used to configure whether a given selection drag event
/// should move or copy the selection. The given predicate will be
/// called with the `mousedown` event, and can return `true` when
/// the drag should move the content.
EditorView.dragMovesSelection = dragMovesSelection;
/// Facet used to configure whether a given selecting click adds
/// a new range to the existing selection or replaces it entirely.
EditorView.clickAddsSelectionRange = clickAddsSelectionRange;
/// Allows you to influence the way mouse selection happens. The
/// functions in this facet will be called for a `mousedown` event
/// on the editor, and can return an object that overrides the way a
/// selection is computed from that mouse click or drag.
EditorView.mouseSelectionStyle = mouseSelectionStyle;
/// A facet that determines which [decorations](#view.Decoration)
/// are shown in the view. See also [view
/// plugins](#view.EditorView^decorations), which have a separate
/// mechanism for providing decorations.
EditorView.decorations = decorations;
/// An extension that enables line wrapping in the editor.
EditorView.lineWrapping = EditorView.theme({ content: { whiteSpace: "pre-wrap" } });
/// Facet that provides attributes for the editor's editable DOM
/// element.
EditorView.contentAttributes = contentAttributes;
/// Facet that provides editor DOM attributes for the editor's
/// outer element.
EditorView.editorAttributes = editorAttributes;
// Maximum line length for which we compute accurate bidi info
const MaxBidiLine = 4096;
function ensureTop(given, dom) {
return given == null ? dom.getBoundingClientRect().top : given;
}
let resizeDebounce = -1;
function ensureGlobalHandler() {
window.addEventListener("resize", () => {
if (resizeDebounce == -1)
resizeDebounce = setTimeout(handleResize, 50);
});
}
function handleResize() {
resizeDebounce = -1;
let found = document.querySelectorAll(".cm-content");
for (let i = 0; i < found.length; i++) {
let docView = ContentView.get(found[i]);
if (docView)
docView.editorView.requestMeasure();
}
}
const BadMeasure = {};
class CachedOrder {
constructor(from, to, dir, order) {
this.from = from;
this.to = to;
this.dir = dir;
this.order = order;
}
static update(cache, changes) {
if (changes.empty)
return cache;
let result = [], lastDir = cache.length ? cache[cache.length - 1].dir : Direction.LTR;
for (let i = Math.max(0, cache.length - 10); i < cache.length; i++) {
let entry = cache[i];
if (entry.dir == lastDir && !changes.touchesRange(entry.from, entry.to))
result.push(new CachedOrder(changes.mapPos(entry.from, 1), changes.mapPos(entry.to, -1), entry.dir, entry.order));
}
return result;
}
}
const currentPlatform = typeof navigator == "undefined" ? "key"
: /Mac/.test(navigator.platform) ? "mac"
: /Win/.test(navigator.platform) ? "win"
: /Linux|X11/.test(navigator.platform) ? "linux"
: "key";
function normalizeKeyName(name, platform) {
const parts = name.split(/-(?!$)/);
let result = parts[parts.length - 1];
if (result == "Space")
result = " ";
let alt, ctrl, shift, meta;
for (let i = 0; i < parts.length - 1; ++i) {
const mod = parts[i];
if (/^(cmd|meta|m)$/i.test(mod))
meta = true;
else if (/^a(lt)?$/i.test(mod))
alt = true;
else if (/^(c|ctrl|control)$/i.test(mod))
ctrl = true;
else if (/^s(hift)?$/i.test(mod))
shift = true;
else if (/^mod$/i.test(mod)) {
if (platform == "mac")
meta = true;
else
ctrl = true;
}
else
throw new Error("Unrecognized modifier name: " + mod);
}
if (alt)
result = "Alt-" + result;
if (ctrl)
result = "Ctrl-" + result;
if (meta)
result = "Meta-" + result;
if (shift)
result = "Shift-" + result;
return result;
}
function modifiers(name, event, shift) {
if (event.altKey)
name = "Alt-" + name;
if (event.ctrlKey)
name = "Ctrl-" + name;
if (event.metaKey)
name = "Meta-" + name;
if (shift !== false && event.shiftKey)
name = "Shift-" + name;
return name;
}
const keymaps = Facet.define();
const handleKeyEvents = EditorView.domEventHandlers({
keydown(event, view) {
return runHandlers(view.state.facet(keymaps), event, view, "editor");
}
});
/// Create a view extension that registers a keymap.
///
/// You can add multiple keymap extensions to an editor. Their
/// priorities determine their precedence (the ones specified early or
/// with high priority get checked first). When a handler has returned
/// `true` for a given key, no further handlers are called.
///
/// When a key is bound multiple times (either in a single keymap or
/// in separate maps), the bound commands all get a chance to handle
/// the key stroke, in order of precedence, until one of them returns
/// true.
function keymap(bindings, platform) {
return [handleKeyEvents, keymaps.of(buildKeymap(bindings, platform))];
}
/// Run the key handlers registered for a given scope. Returns true if
/// any of them handled the event.
function runScopeHandlers(view, event, scope) {
return runHandlers(view.state.facet(keymaps), event, view, scope);
}
let storedPrefix = null;
const PrefixTimeout = 4000;
function buildKeymap(bindings, platform = currentPlatform) {
let bound = Object.create(null);
let isPrefix = Object.create(null);
let checkPrefix = (name, is) => {
let current = isPrefix[name];
if (current == null)
isPrefix[name] = is;
else if (current != is)
throw new Error("Key binding " + name + " is used both as a regular binding and as a multi-stroke prefix");
};
let add = (scope, key, command, preventDefault) => {
let scopeObj = bound[scope] || (bound[scope] = Object.create(null));
let parts = key.split(/ (?!$)/).map(k => normalizeKeyName(k, platform));
for (let i = 1; i < parts.length; i++) {
let prefix = parts.slice(0, i).join(" ");
checkPrefix(prefix, true);
if (!scopeObj[prefix])
scopeObj[prefix] = {
preventDefault: true,
commands: [(view) => {
let ourObj = storedPrefix = { view, prefix, scope };
setTimeout(() => { if (storedPrefix == ourObj)
storedPrefix = null; }, PrefixTimeout);
return true;
}]
};
}
let full = parts.join(" ");
checkPrefix(full, false);
let binding = scopeObj[full] || (scopeObj[full] = { preventDefault: false, commands: [] });
binding.commands.push(command);
if (preventDefault)
binding.preventDefault = true;
};
for (let b of bindings) {
let name = b[platform] || b.key;
if (!name)
continue;
for (let scope of b.scope ? b.scope.split(" ") : ["editor"]) {
add(scope, name, b.run, b.preventDefault);
if (b.shift)
add(scope, "Shift-" + name, b.shift, b.preventDefault);
}
}
return bound;
}
function runHandlers(maps, event, view, scope) {
let name = keyName(event), isChar = name.length == 1 && name != " ";
let prefix = "";
if (storedPrefix && storedPrefix.view == view && storedPrefix.scope == scope) {
prefix = storedPrefix.prefix + " ";
storedPrefix = null;
}
let fallthrough = !!prefix;
let runFor = (binding) => {
if (binding) {
for (let cmd of binding.commands)
if (cmd(view))
return true;
if (binding.preventDefault)
fallthrough = true;
}
return false;
};
for (let map of maps) {
let scopeObj = map[scope], baseName;
if (!scopeObj)
continue;
if (runFor(scopeObj[prefix + modifiers(name, event, !isChar)]))
return true;
if (isChar && (event.shiftKey || event.altKey || event.metaKey) &&
(baseName = base[event.keyCode]) && baseName != name) {
if (runFor(scopeObj[prefix + modifiers(baseName, event, true)]))
return true;
}
else if (isChar && event.shiftKey) {
if (runFor(scopeObj[prefix + modifiers(name, event, true)]))
return true;
}
}
return fallthrough;
}
const field = StateField.define({
create(state) {
return decorateSelections(state.selection);
},
update(deco, tr) {
return tr.docChanged || tr.selection ? decorateSelections(tr.state.selection) : deco;
},
provide: [EditorView.decorations]
});
/// Returns an extension that enables multiple selections for the
/// editor. Secondary cursors and selected ranges are drawn with
/// simple decorations, and might not look the same as the primary
/// native selection.
function multipleSelections() {
return [
EditorState.allowMultipleSelections.of(true),
field
];
}
class CursorWidget extends WidgetType {
toDOM() {
let span = document.createElement("span");
span.className = themeClass("secondaryCursor");
return span;
}
}
CursorWidget.deco = Decoration.widget({ widget: new CursorWidget(null) });
const rangeMark = Decoration.mark({ class: themeClass("secondarySelection") });
function decorateSelections(selection) {
let { ranges, primaryIndex } = selection;
if (ranges.length == 1)
return Decoration.none;
let deco = [];
for (let i = 0; i < ranges.length; i++)
if (i != primaryIndex) {
let range = ranges[i];
deco.push(range.empty ? CursorWidget.deco.range(range.from) : rangeMark.range(ranges[i].from, ranges[i].to));
}
return Decoration.set(deco);
}
const Specials = /[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200c\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/gu;
const Names = {
0: "null",
7: "bell",
8: "backspace",
10: "newline",
11: "vertical tab",
13: "carriage return",
27: "escape",
8203: "zero width space",
8204: "zero width non-joiner",
8205: "zero width joiner",
8206: "left-to-right mark",
8207: "right-to-left mark",
8232: "line separator",
8233: "paragraph separator",
65279: "zero width no-break space",
65532: "object replacement"
};
let _supportsTabSize = null;
function supportsTabSize() {
if (_supportsTabSize == null && typeof document != "undefined" && document.body) {
let styles = document.body.style;
_supportsTabSize = (styles.tabSize || styles.MozTabSize) != null;
}
return _supportsTabSize || false;
}
const UnicodeRegexpSupport = /x/.unicode != null ? "gu" : "g";
const specialCharConfig = Facet.define({
combine(configs) {
// FIXME make configurations compose properly
let config = combineConfig(configs, {
render: null,
specialChars: Specials,
addSpecialChars: null
});
if (config.replaceTabs = !supportsTabSize())
config.specialChars = new RegExp("\t|" + config.specialChars.source, UnicodeRegexpSupport);
if (config.addSpecialChars)
config.specialChars = new RegExp(config.specialChars.source + "|" + config.addSpecialChars.source, UnicodeRegexpSupport);
return config;
}
});
/// Returns an extension that installs highlighting of special
/// characters.
function highlightSpecialChars(
/// Configuration options.
config = {}) {
let ext = [specialCharConfig.of(config), specialCharPlugin];
if (!supportsTabSize())
ext.push(tabStyleExt);
return ext;
}
const specialCharPlugin = ViewPlugin.fromClass(class {
constructor(view) {
this.view = view;
this.decorations = Decoration.none;
this.decorationCache = Object.create(null);
this.recompute();
}
update(update) {
let confChange = update.prevState.facet(specialCharConfig) != update.state.facet(specialCharConfig);
if (confChange)
this.decorationCache = Object.create(null);
if (confChange || update.changes.length || update.viewportChanged)
this.recompute();
}
recompute() {
let decorations = [];
for (let { from, to } of this.view.visibleRanges)
this.getDecorationsFor(from, to, decorations);
this.decorations = Decoration.set(decorations);
}
getDecorationsFor(from, to, target) {
let config = this.view.state.facet(specialCharConfig);
let { doc } = this.view.state;
for (let pos = from, cursor = doc.iterRange(from, to), m; !cursor.next().done;) {
if (!cursor.lineBreak) {
while (m = config.specialChars.exec(cursor.value)) {
let code = codePointAt(m[0], 0), deco;
if (code == null)
continue;
if (code == 9) {
let line = doc.lineAt(pos + m.index);
let size = this.view.state.tabSize, col = countColumn(doc.sliceString(line.from, pos + m.index), 0, size);
deco = Decoration.replace({ widget: new TabWidget((size - (col % size)) * this.view.defaultCharacterWidth) });
}
else {
deco = this.decorationCache[code] ||
(this.decorationCache[code] = Decoration.replace({ widget: new SpecialCharWidget(config, code) }));
}
target.push(deco.range(pos + m.index, pos + m.index + m[0].length));
}
}
pos += cursor.value.length;
}
}
}).decorations();
// Assigns placeholder characters from the Control Pictures block to
// ASCII control characters
function placeHolder(code) {
if (code >= 32)
return null;
if (code == 10)
return "\u2424";
return String.fromCharCode(9216 + code);
}
const DefaultPlaceholder = "\u2022";
class SpecialCharWidget extends WidgetType {
constructor(options, code) {
super(code);
this.options = options;
}
toDOM() {
let ph = placeHolder(this.value) || DefaultPlaceholder;
let desc = "Control character " + (Names[this.value] || this.value);
let custom = this.options.render && this.options.render(this.value, desc, ph);
if (custom)
return custom;
let span = document.createElement("span");
span.textContent = ph;
span.title = desc;
span.setAttribute("aria-label", desc);
span.style.color = "red";
return span;
}
ignoreEvent() { return false; }
}
class TabWidget extends WidgetType {
toDOM() {
let span = document.createElement("span");
span.textContent = "\t";
span.className = tabStyle.tab;
span.style.width = this.value + "px";
return span;
}
ignoreEvent() { return false; }
}
const tabStyle = new StyleModule({
tab: {
display: "inline-block",
overflow: "hidden",
verticalAlign: "bottom"
}
});
const tabStyleExt = EditorView.styleModule.of(tabStyle);
/// @internal
const __test = { HeightMap, HeightOracle, MeasuredHeights, QueryType, ChangedRange, computeOrder, moveVisually };
export { BidiSpan, BlockInfo, BlockType, Decoration, Direction, EditorView, PluginField, ViewPlugin, ViewUpdate, WidgetType, __test, highlightSpecialChars, keymap, logException, multipleSelections, runScopeHandlers, themeClass };
//# sourceMappingURL=index.js.map
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment