mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-11-16 23:04:50 +00:00
dd8c8567fd
The plugin is incomplete, and currently abandoned. It loads up an instance of CodeMirror as an editor, and loads the right tiddler into it, but the work hasn't been done to allow changes to be saved. The startup time is ridiculously long, which is why I abandoned it.
5385 lines
204 KiB
JavaScript
5385 lines
204 KiB
JavaScript
// CodeMirror version 3.1
|
|
//
|
|
// CodeMirror is the only global var we claim
|
|
window.CodeMirror = (function() {
|
|
"use strict";
|
|
|
|
// BROWSER SNIFFING
|
|
|
|
// Crude, but necessary to handle a number of hard-to-feature-detect
|
|
// bugs and behavior differences.
|
|
var gecko = /gecko\/\d/i.test(navigator.userAgent);
|
|
var ie = /MSIE \d/.test(navigator.userAgent);
|
|
var ie_lt8 = ie && (document.documentMode == null || document.documentMode < 8);
|
|
var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9);
|
|
var webkit = /WebKit\//.test(navigator.userAgent);
|
|
var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent);
|
|
var chrome = /Chrome\//.test(navigator.userAgent);
|
|
var opera = /Opera\//.test(navigator.userAgent);
|
|
var safari = /Apple Computer/.test(navigator.vendor);
|
|
var khtml = /KHTML\//.test(navigator.userAgent);
|
|
var mac_geLion = /Mac OS X 1\d\D([7-9]|\d\d)\D/.test(navigator.userAgent);
|
|
var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent);
|
|
var phantom = /PhantomJS/.test(navigator.userAgent);
|
|
|
|
var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent);
|
|
// This is woefully incomplete. Suggestions for alternative methods welcome.
|
|
var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);
|
|
var mac = ios || /Mac/.test(navigator.platform);
|
|
var windows = /windows/i.test(navigator.platform);
|
|
|
|
var opera_version = opera && navigator.userAgent.match(/Version\/(\d*\.\d*)/);
|
|
if (opera_version) opera_version = Number(opera_version[1]);
|
|
// Some browsers use the wrong event properties to signal cmd/ctrl on OS X
|
|
var flipCtrlCmd = mac && (qtwebkit || opera && (opera_version == null || opera_version < 12.11));
|
|
var captureMiddleClick = gecko || (ie && !ie_lt9);
|
|
|
|
// Optimize some code when these features are not used
|
|
var sawReadOnlySpans = false, sawCollapsedSpans = false;
|
|
|
|
// CONSTRUCTOR
|
|
|
|
function CodeMirror(place, options) {
|
|
if (!(this instanceof CodeMirror)) return new CodeMirror(place, options);
|
|
|
|
this.options = options = options || {};
|
|
// Determine effective options based on given values and defaults.
|
|
for (var opt in defaults) if (!options.hasOwnProperty(opt) && defaults.hasOwnProperty(opt))
|
|
options[opt] = defaults[opt];
|
|
setGuttersForLineNumbers(options);
|
|
|
|
var docStart = typeof options.value == "string" ? 0 : options.value.first;
|
|
var display = this.display = makeDisplay(place, docStart);
|
|
display.wrapper.CodeMirror = this;
|
|
updateGutters(this);
|
|
if (options.autofocus && !mobile) focusInput(this);
|
|
|
|
this.state = {keyMaps: [],
|
|
overlays: [],
|
|
modeGen: 0,
|
|
overwrite: false, focused: false,
|
|
suppressEdits: false, pasteIncoming: false,
|
|
draggingText: false,
|
|
highlight: new Delayed()};
|
|
|
|
themeChanged(this);
|
|
if (options.lineWrapping)
|
|
this.display.wrapper.className += " CodeMirror-wrap";
|
|
|
|
var doc = options.value;
|
|
if (typeof doc == "string") doc = new Doc(options.value, options.mode);
|
|
operation(this, attachDoc)(this, doc);
|
|
|
|
// Override magic textarea content restore that IE sometimes does
|
|
// on our hidden textarea on reload
|
|
if (ie) setTimeout(bind(resetInput, this, true), 20);
|
|
|
|
registerEventHandlers(this);
|
|
// IE throws unspecified error in certain cases, when
|
|
// trying to access activeElement before onload
|
|
var hasFocus; try { hasFocus = (document.activeElement == display.input); } catch(e) { }
|
|
if (hasFocus || (options.autofocus && !mobile)) setTimeout(bind(onFocus, this), 20);
|
|
else onBlur(this);
|
|
|
|
operation(this, function() {
|
|
for (var opt in optionHandlers)
|
|
if (optionHandlers.propertyIsEnumerable(opt))
|
|
optionHandlers[opt](this, options[opt], Init);
|
|
for (var i = 0; i < initHooks.length; ++i) initHooks[i](this);
|
|
})();
|
|
}
|
|
|
|
// DISPLAY CONSTRUCTOR
|
|
|
|
function makeDisplay(place, docStart) {
|
|
var d = {};
|
|
var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none;");
|
|
if (webkit) input.style.width = "1000px";
|
|
else input.setAttribute("wrap", "off");
|
|
input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off");
|
|
// Wraps and hides input textarea
|
|
d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
|
|
// The actual fake scrollbars.
|
|
d.scrollbarH = elt("div", [elt("div", null, null, "height: 1px")], "CodeMirror-hscrollbar");
|
|
d.scrollbarV = elt("div", [elt("div", null, null, "width: 1px")], "CodeMirror-vscrollbar");
|
|
d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler");
|
|
// DIVs containing the selection and the actual code
|
|
d.lineDiv = elt("div");
|
|
d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1");
|
|
// Blinky cursor, and element used to ensure cursor fits at the end of a line
|
|
d.cursor = elt("div", "\u00a0", "CodeMirror-cursor");
|
|
// Secondary cursor, shown when on a 'jump' in bi-directional text
|
|
d.otherCursor = elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor");
|
|
// Used to measure text size
|
|
d.measure = elt("div", null, "CodeMirror-measure");
|
|
// Wraps everything that needs to exist inside the vertically-padded coordinate system
|
|
d.lineSpace = elt("div", [d.measure, d.selectionDiv, d.lineDiv, d.cursor, d.otherCursor],
|
|
null, "position: relative; outline: none");
|
|
// Moved around its parent to cover visible view
|
|
d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative");
|
|
// Set to the height of the text, causes scrolling
|
|
d.sizer = elt("div", [d.mover], "CodeMirror-sizer");
|
|
// D is needed because behavior of elts with overflow: auto and padding is inconsistent across browsers
|
|
d.heightForcer = elt("div", "\u00a0", null, "position: absolute; height: " + scrollerCutOff + "px");
|
|
// Will contain the gutters, if any
|
|
d.gutters = elt("div", null, "CodeMirror-gutters");
|
|
d.lineGutter = null;
|
|
// Helper element to properly size the gutter backgrounds
|
|
var scrollerInner = elt("div", [d.sizer, d.heightForcer, d.gutters], null, "position: relative; min-height: 100%");
|
|
// Provides scrolling
|
|
d.scroller = elt("div", [scrollerInner], "CodeMirror-scroll");
|
|
d.scroller.setAttribute("tabIndex", "-1");
|
|
// The element in which the editor lives.
|
|
d.wrapper = elt("div", [d.inputDiv, d.scrollbarH, d.scrollbarV,
|
|
d.scrollbarFiller, d.scroller], "CodeMirror");
|
|
// Work around IE7 z-index bug
|
|
if (ie_lt8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; }
|
|
if (place.appendChild) place.appendChild(d.wrapper); else place(d.wrapper);
|
|
|
|
// Needed to hide big blue blinking cursor on Mobile Safari
|
|
if (ios) input.style.width = "0px";
|
|
if (!webkit) d.scroller.draggable = true;
|
|
// Needed to handle Tab key in KHTML
|
|
if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; }
|
|
// Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
|
|
else if (ie_lt8) d.scrollbarH.style.minWidth = d.scrollbarV.style.minWidth = "18px";
|
|
|
|
// Current visible range (may be bigger than the view window).
|
|
d.viewOffset = d.lastSizeC = 0;
|
|
d.showingFrom = d.showingTo = docStart;
|
|
|
|
// Used to only resize the line number gutter when necessary (when
|
|
// the amount of lines crosses a boundary that makes its width change)
|
|
d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null;
|
|
// See readInput and resetInput
|
|
d.prevInput = "";
|
|
// Set to true when a non-horizontal-scrolling widget is added. As
|
|
// an optimization, widget aligning is skipped when d is false.
|
|
d.alignWidgets = false;
|
|
// Flag that indicates whether we currently expect input to appear
|
|
// (after some event like 'keypress' or 'input') and are polling
|
|
// intensively.
|
|
d.pollingFast = false;
|
|
// Self-resetting timeout for the poller
|
|
d.poll = new Delayed();
|
|
// True when a drag from the editor is active
|
|
d.draggingText = false;
|
|
|
|
d.cachedCharWidth = d.cachedTextHeight = null;
|
|
d.measureLineCache = [];
|
|
d.measureLineCachePos = 0;
|
|
|
|
// Tracks when resetInput has punted to just putting a short
|
|
// string instead of the (large) selection.
|
|
d.inaccurateSelection = false;
|
|
|
|
// Tracks the maximum line length so that the horizontal scrollbar
|
|
// can be kept static when scrolling.
|
|
d.maxLine = null;
|
|
d.maxLineLength = 0;
|
|
d.maxLineChanged = false;
|
|
|
|
// Used for measuring wheel scrolling granularity
|
|
d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null;
|
|
|
|
return d;
|
|
}
|
|
|
|
// STATE UPDATES
|
|
|
|
// Used to get the editor into a consistent state again when options change.
|
|
|
|
function loadMode(cm) {
|
|
cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption);
|
|
cm.doc.iter(function(line) {
|
|
if (line.stateAfter) line.stateAfter = null;
|
|
if (line.styles) line.styles = null;
|
|
});
|
|
cm.doc.frontier = cm.doc.first;
|
|
startWorker(cm, 100);
|
|
cm.state.modeGen++;
|
|
if (cm.curOp) regChange(cm);
|
|
}
|
|
|
|
function wrappingChanged(cm) {
|
|
if (cm.options.lineWrapping) {
|
|
cm.display.wrapper.className += " CodeMirror-wrap";
|
|
cm.display.sizer.style.minWidth = "";
|
|
} else {
|
|
cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-wrap", "");
|
|
computeMaxLength(cm);
|
|
}
|
|
estimateLineHeights(cm);
|
|
regChange(cm);
|
|
clearCaches(cm);
|
|
setTimeout(function(){updateScrollbars(cm.display, cm.doc.height);}, 100);
|
|
}
|
|
|
|
function estimateHeight(cm) {
|
|
var th = textHeight(cm.display), wrapping = cm.options.lineWrapping;
|
|
var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3);
|
|
return function(line) {
|
|
if (lineIsHidden(cm.doc, line))
|
|
return 0;
|
|
else if (wrapping)
|
|
return (Math.ceil(line.text.length / perLine) || 1) * th;
|
|
else
|
|
return th;
|
|
};
|
|
}
|
|
|
|
function estimateLineHeights(cm) {
|
|
var doc = cm.doc, est = estimateHeight(cm);
|
|
doc.iter(function(line) {
|
|
var estHeight = est(line);
|
|
if (estHeight != line.height) updateLineHeight(line, estHeight);
|
|
});
|
|
}
|
|
|
|
function keyMapChanged(cm) {
|
|
var style = keyMap[cm.options.keyMap].style;
|
|
cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-keymap-\S+/g, "") +
|
|
(style ? " cm-keymap-" + style : "");
|
|
}
|
|
|
|
function themeChanged(cm) {
|
|
cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") +
|
|
cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-");
|
|
clearCaches(cm);
|
|
}
|
|
|
|
function guttersChanged(cm) {
|
|
updateGutters(cm);
|
|
regChange(cm);
|
|
}
|
|
|
|
function updateGutters(cm) {
|
|
var gutters = cm.display.gutters, specs = cm.options.gutters;
|
|
removeChildren(gutters);
|
|
for (var i = 0; i < specs.length; ++i) {
|
|
var gutterClass = specs[i];
|
|
var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass));
|
|
if (gutterClass == "CodeMirror-linenumbers") {
|
|
cm.display.lineGutter = gElt;
|
|
gElt.style.width = (cm.display.lineNumWidth || 1) + "px";
|
|
}
|
|
}
|
|
gutters.style.display = i ? "" : "none";
|
|
}
|
|
|
|
function lineLength(doc, line) {
|
|
if (line.height == 0) return 0;
|
|
var len = line.text.length, merged, cur = line;
|
|
while (merged = collapsedSpanAtStart(cur)) {
|
|
var found = merged.find();
|
|
cur = getLine(doc, found.from.line);
|
|
len += found.from.ch - found.to.ch;
|
|
}
|
|
cur = line;
|
|
while (merged = collapsedSpanAtEnd(cur)) {
|
|
var found = merged.find();
|
|
len -= cur.text.length - found.from.ch;
|
|
cur = getLine(doc, found.to.line);
|
|
len += cur.text.length - found.to.ch;
|
|
}
|
|
return len;
|
|
}
|
|
|
|
function computeMaxLength(cm) {
|
|
var d = cm.display, doc = cm.doc;
|
|
d.maxLine = getLine(doc, doc.first);
|
|
d.maxLineLength = lineLength(doc, d.maxLine);
|
|
d.maxLineChanged = true;
|
|
doc.iter(function(line) {
|
|
var len = lineLength(doc, line);
|
|
if (len > d.maxLineLength) {
|
|
d.maxLineLength = len;
|
|
d.maxLine = line;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Make sure the gutters options contains the element
|
|
// "CodeMirror-linenumbers" when the lineNumbers option is true.
|
|
function setGuttersForLineNumbers(options) {
|
|
var found = false;
|
|
for (var i = 0; i < options.gutters.length; ++i) {
|
|
if (options.gutters[i] == "CodeMirror-linenumbers") {
|
|
if (options.lineNumbers) found = true;
|
|
else options.gutters.splice(i--, 1);
|
|
}
|
|
}
|
|
if (!found && options.lineNumbers)
|
|
options.gutters.push("CodeMirror-linenumbers");
|
|
}
|
|
|
|
// SCROLLBARS
|
|
|
|
// Re-synchronize the fake scrollbars with the actual size of the
|
|
// content. Optionally force a scrollTop.
|
|
function updateScrollbars(d /* display */, docHeight) {
|
|
var totalHeight = docHeight + 2 * paddingTop(d);
|
|
d.sizer.style.minHeight = d.heightForcer.style.top = totalHeight + "px";
|
|
var scrollHeight = Math.max(totalHeight, d.scroller.scrollHeight);
|
|
var needsH = d.scroller.scrollWidth > d.scroller.clientWidth;
|
|
var needsV = scrollHeight > d.scroller.clientHeight;
|
|
if (needsV) {
|
|
d.scrollbarV.style.display = "block";
|
|
d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0";
|
|
d.scrollbarV.firstChild.style.height =
|
|
(scrollHeight - d.scroller.clientHeight + d.scrollbarV.clientHeight) + "px";
|
|
} else d.scrollbarV.style.display = "";
|
|
if (needsH) {
|
|
d.scrollbarH.style.display = "block";
|
|
d.scrollbarH.style.right = needsV ? scrollbarWidth(d.measure) + "px" : "0";
|
|
d.scrollbarH.firstChild.style.width =
|
|
(d.scroller.scrollWidth - d.scroller.clientWidth + d.scrollbarH.clientWidth) + "px";
|
|
} else d.scrollbarH.style.display = "";
|
|
if (needsH && needsV) {
|
|
d.scrollbarFiller.style.display = "block";
|
|
d.scrollbarFiller.style.height = d.scrollbarFiller.style.width = scrollbarWidth(d.measure) + "px";
|
|
} else d.scrollbarFiller.style.display = "";
|
|
|
|
if (mac_geLion && scrollbarWidth(d.measure) === 0)
|
|
d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = mac_geMountainLion ? "18px" : "12px";
|
|
}
|
|
|
|
function visibleLines(display, doc, viewPort) {
|
|
var top = display.scroller.scrollTop, height = display.wrapper.clientHeight;
|
|
if (typeof viewPort == "number") top = viewPort;
|
|
else if (viewPort) {top = viewPort.top; height = viewPort.bottom - viewPort.top;}
|
|
top = Math.floor(top - paddingTop(display));
|
|
var bottom = Math.ceil(top + height);
|
|
return {from: lineAtHeight(doc, top), to: lineAtHeight(doc, bottom)};
|
|
}
|
|
|
|
// LINE NUMBERS
|
|
|
|
function alignHorizontally(cm) {
|
|
var display = cm.display;
|
|
if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return;
|
|
var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft;
|
|
var gutterW = display.gutters.offsetWidth, l = comp + "px";
|
|
for (var n = display.lineDiv.firstChild; n; n = n.nextSibling) if (n.alignable) {
|
|
for (var i = 0, a = n.alignable; i < a.length; ++i) a[i].style.left = l;
|
|
}
|
|
if (cm.options.fixedGutter)
|
|
display.gutters.style.left = (comp + gutterW) + "px";
|
|
}
|
|
|
|
function maybeUpdateLineNumberWidth(cm) {
|
|
if (!cm.options.lineNumbers) return false;
|
|
var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display;
|
|
if (last.length != display.lineNumChars) {
|
|
var test = display.measure.appendChild(elt("div", [elt("div", last)],
|
|
"CodeMirror-linenumber CodeMirror-gutter-elt"));
|
|
var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW;
|
|
display.lineGutter.style.width = "";
|
|
display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding);
|
|
display.lineNumWidth = display.lineNumInnerWidth + padding;
|
|
display.lineNumChars = display.lineNumInnerWidth ? last.length : -1;
|
|
display.lineGutter.style.width = display.lineNumWidth + "px";
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function lineNumberFor(options, i) {
|
|
return String(options.lineNumberFormatter(i + options.firstLineNumber));
|
|
}
|
|
function compensateForHScroll(display) {
|
|
return getRect(display.scroller).left - getRect(display.sizer).left;
|
|
}
|
|
|
|
// DISPLAY DRAWING
|
|
|
|
function updateDisplay(cm, changes, viewPort) {
|
|
var oldFrom = cm.display.showingFrom, oldTo = cm.display.showingTo;
|
|
var updated = updateDisplayInner(cm, changes, viewPort);
|
|
if (updated) {
|
|
signalLater(cm, "update", cm);
|
|
if (cm.display.showingFrom != oldFrom || cm.display.showingTo != oldTo)
|
|
signalLater(cm, "viewportChange", cm, cm.display.showingFrom, cm.display.showingTo);
|
|
}
|
|
updateSelection(cm);
|
|
updateScrollbars(cm.display, cm.doc.height);
|
|
|
|
return updated;
|
|
}
|
|
|
|
// Uses a set of changes plus the current scroll position to
|
|
// determine which DOM updates have to be made, and makes the
|
|
// updates.
|
|
function updateDisplayInner(cm, changes, viewPort) {
|
|
var display = cm.display, doc = cm.doc;
|
|
if (!display.wrapper.clientWidth) {
|
|
display.showingFrom = display.showingTo = doc.first;
|
|
display.viewOffset = 0;
|
|
return;
|
|
}
|
|
|
|
// Compute the new visible window
|
|
// If scrollTop is specified, use that to determine which lines
|
|
// to render instead of the current scrollbar position.
|
|
var visible = visibleLines(display, doc, viewPort);
|
|
// Bail out if the visible area is already rendered and nothing changed.
|
|
if (changes.length == 0 &&
|
|
visible.from > display.showingFrom && visible.to < display.showingTo)
|
|
return;
|
|
|
|
if (maybeUpdateLineNumberWidth(cm))
|
|
changes = [{from: doc.first, to: doc.first + doc.size}];
|
|
var gutterW = display.sizer.style.marginLeft = display.gutters.offsetWidth + "px";
|
|
display.scrollbarH.style.left = cm.options.fixedGutter ? gutterW : "0";
|
|
|
|
// Used to determine which lines need their line numbers updated
|
|
var positionsChangedFrom = Infinity;
|
|
if (cm.options.lineNumbers)
|
|
for (var i = 0; i < changes.length; ++i)
|
|
if (changes[i].diff) { positionsChangedFrom = changes[i].from; break; }
|
|
|
|
var end = doc.first + doc.size;
|
|
var from = Math.max(visible.from - cm.options.viewportMargin, doc.first);
|
|
var to = Math.min(end, visible.to + cm.options.viewportMargin);
|
|
if (display.showingFrom < from && from - display.showingFrom < 20) from = Math.max(doc.first, display.showingFrom);
|
|
if (display.showingTo > to && display.showingTo - to < 20) to = Math.min(end, display.showingTo);
|
|
if (sawCollapsedSpans) {
|
|
from = lineNo(visualLine(doc, getLine(doc, from)));
|
|
while (to < end && lineIsHidden(doc, getLine(doc, to))) ++to;
|
|
}
|
|
|
|
// Create a range of theoretically intact lines, and punch holes
|
|
// in that using the change info.
|
|
var intact = [{from: Math.max(display.showingFrom, doc.first),
|
|
to: Math.min(display.showingTo, end)}];
|
|
if (intact[0].from >= intact[0].to) intact = [];
|
|
else intact = computeIntact(intact, changes);
|
|
// When merged lines are present, we might have to reduce the
|
|
// intact ranges because changes in continued fragments of the
|
|
// intact lines do require the lines to be redrawn.
|
|
if (sawCollapsedSpans)
|
|
for (var i = 0; i < intact.length; ++i) {
|
|
var range = intact[i], merged;
|
|
while (merged = collapsedSpanAtEnd(getLine(doc, range.to - 1))) {
|
|
var newTo = merged.find().from.line;
|
|
if (newTo > range.from) range.to = newTo;
|
|
else { intact.splice(i--, 1); break; }
|
|
}
|
|
}
|
|
|
|
// Clip off the parts that won't be visible
|
|
var intactLines = 0;
|
|
for (var i = 0; i < intact.length; ++i) {
|
|
var range = intact[i];
|
|
if (range.from < from) range.from = from;
|
|
if (range.to > to) range.to = to;
|
|
if (range.from >= range.to) intact.splice(i--, 1);
|
|
else intactLines += range.to - range.from;
|
|
}
|
|
if (intactLines == to - from && from == display.showingFrom && to == display.showingTo) {
|
|
updateViewOffset(cm);
|
|
return;
|
|
}
|
|
intact.sort(function(a, b) {return a.from - b.from;});
|
|
|
|
var focused = document.activeElement;
|
|
if (intactLines < (to - from) * .7) display.lineDiv.style.display = "none";
|
|
patchDisplay(cm, from, to, intact, positionsChangedFrom);
|
|
display.lineDiv.style.display = "";
|
|
if (document.activeElement != focused && focused.offsetHeight) focused.focus();
|
|
|
|
var different = from != display.showingFrom || to != display.showingTo ||
|
|
display.lastSizeC != display.wrapper.clientHeight;
|
|
// This is just a bogus formula that detects when the editor is
|
|
// resized or the font size changes.
|
|
if (different) display.lastSizeC = display.wrapper.clientHeight;
|
|
display.showingFrom = from; display.showingTo = to;
|
|
startWorker(cm, 100);
|
|
|
|
var prevBottom = display.lineDiv.offsetTop;
|
|
for (var node = display.lineDiv.firstChild, height; node; node = node.nextSibling) if (node.lineObj) {
|
|
if (ie_lt8) {
|
|
var bot = node.offsetTop + node.offsetHeight;
|
|
height = bot - prevBottom;
|
|
prevBottom = bot;
|
|
} else {
|
|
var box = getRect(node);
|
|
height = box.bottom - box.top;
|
|
}
|
|
var diff = node.lineObj.height - height;
|
|
if (height < 2) height = textHeight(display);
|
|
if (diff > .001 || diff < -.001) {
|
|
updateLineHeight(node.lineObj, height);
|
|
var widgets = node.lineObj.widgets;
|
|
if (widgets) for (var i = 0; i < widgets.length; ++i)
|
|
widgets[i].height = widgets[i].node.offsetHeight;
|
|
}
|
|
}
|
|
updateViewOffset(cm);
|
|
|
|
if (visibleLines(display, doc, viewPort).to > to)
|
|
updateDisplayInner(cm, [], viewPort);
|
|
return true;
|
|
}
|
|
|
|
function updateViewOffset(cm) {
|
|
var off = cm.display.viewOffset = heightAtLine(cm, getLine(cm.doc, cm.display.showingFrom));
|
|
// Position the mover div to align with the current virtual scroll position
|
|
cm.display.mover.style.top = off + "px";
|
|
}
|
|
|
|
function computeIntact(intact, changes) {
|
|
for (var i = 0, l = changes.length || 0; i < l; ++i) {
|
|
var change = changes[i], intact2 = [], diff = change.diff || 0;
|
|
for (var j = 0, l2 = intact.length; j < l2; ++j) {
|
|
var range = intact[j];
|
|
if (change.to <= range.from && change.diff) {
|
|
intact2.push({from: range.from + diff, to: range.to + diff});
|
|
} else if (change.to <= range.from || change.from >= range.to) {
|
|
intact2.push(range);
|
|
} else {
|
|
if (change.from > range.from)
|
|
intact2.push({from: range.from, to: change.from});
|
|
if (change.to < range.to)
|
|
intact2.push({from: change.to + diff, to: range.to + diff});
|
|
}
|
|
}
|
|
intact = intact2;
|
|
}
|
|
return intact;
|
|
}
|
|
|
|
function getDimensions(cm) {
|
|
var d = cm.display, left = {}, width = {};
|
|
for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) {
|
|
left[cm.options.gutters[i]] = n.offsetLeft;
|
|
width[cm.options.gutters[i]] = n.offsetWidth;
|
|
}
|
|
return {fixedPos: compensateForHScroll(d),
|
|
gutterTotalWidth: d.gutters.offsetWidth,
|
|
gutterLeft: left,
|
|
gutterWidth: width,
|
|
wrapperWidth: d.wrapper.clientWidth};
|
|
}
|
|
|
|
function patchDisplay(cm, from, to, intact, updateNumbersFrom) {
|
|
var dims = getDimensions(cm);
|
|
var display = cm.display, lineNumbers = cm.options.lineNumbers;
|
|
if (!intact.length && (!webkit || !cm.display.currentWheelTarget))
|
|
removeChildren(display.lineDiv);
|
|
var container = display.lineDiv, cur = container.firstChild;
|
|
|
|
function rm(node) {
|
|
var next = node.nextSibling;
|
|
if (webkit && mac && cm.display.currentWheelTarget == node) {
|
|
node.style.display = "none";
|
|
node.lineObj = null;
|
|
} else {
|
|
node.parentNode.removeChild(node);
|
|
}
|
|
return next;
|
|
}
|
|
|
|
var nextIntact = intact.shift(), lineN = from;
|
|
cm.doc.iter(from, to, function(line) {
|
|
if (nextIntact && nextIntact.to == lineN) nextIntact = intact.shift();
|
|
if (lineIsHidden(cm.doc, line)) {
|
|
if (line.height != 0) updateLineHeight(line, 0);
|
|
if (line.widgets && cur.previousSibling) for (var i = 0; i < line.widgets.length; ++i)
|
|
if (line.widgets[i].showIfHidden) {
|
|
var prev = cur.previousSibling;
|
|
if (/pre/i.test(prev.nodeName)) {
|
|
var wrap = elt("div", null, null, "position: relative");
|
|
prev.parentNode.replaceChild(wrap, prev);
|
|
wrap.appendChild(prev);
|
|
prev = wrap;
|
|
}
|
|
var wnode = prev.appendChild(elt("div", [line.widgets[i].node], "CodeMirror-linewidget"));
|
|
positionLineWidget(line.widgets[i], wnode, prev, dims);
|
|
}
|
|
} else if (nextIntact && nextIntact.from <= lineN && nextIntact.to > lineN) {
|
|
// This line is intact. Skip to the actual node. Update its
|
|
// line number if needed.
|
|
while (cur.lineObj != line) cur = rm(cur);
|
|
if (lineNumbers && updateNumbersFrom <= lineN && cur.lineNumber)
|
|
setTextContent(cur.lineNumber, lineNumberFor(cm.options, lineN));
|
|
cur = cur.nextSibling;
|
|
} else {
|
|
// For lines with widgets, make an attempt to find and reuse
|
|
// the existing element, so that widgets aren't needlessly
|
|
// removed and re-inserted into the dom
|
|
if (line.widgets) for (var j = 0, search = cur, reuse; search && j < 20; ++j, search = search.nextSibling)
|
|
if (search.lineObj == line && /div/i.test(search.nodeName)) { reuse = search; break; }
|
|
// This line needs to be generated.
|
|
var lineNode = buildLineElement(cm, line, lineN, dims, reuse);
|
|
if (lineNode != reuse) {
|
|
container.insertBefore(lineNode, cur);
|
|
} else {
|
|
while (cur != reuse) cur = rm(cur);
|
|
cur = cur.nextSibling;
|
|
}
|
|
|
|
lineNode.lineObj = line;
|
|
}
|
|
++lineN;
|
|
});
|
|
while (cur) cur = rm(cur);
|
|
}
|
|
|
|
function buildLineElement(cm, line, lineNo, dims, reuse) {
|
|
var lineElement = lineContent(cm, line);
|
|
var markers = line.gutterMarkers, display = cm.display, wrap;
|
|
|
|
if (!cm.options.lineNumbers && !markers && !line.bgClass && !line.wrapClass && !line.widgets)
|
|
return lineElement;
|
|
|
|
// Lines with gutter elements, widgets or a background class need
|
|
// to be wrapped again, and have the extra elements added to the
|
|
// wrapper div
|
|
|
|
if (reuse) {
|
|
reuse.alignable = null;
|
|
var isOk = true, widgetsSeen = 0;
|
|
for (var n = reuse.firstChild, next; n; n = next) {
|
|
next = n.nextSibling;
|
|
if (!/\bCodeMirror-linewidget\b/.test(n.className)) {
|
|
reuse.removeChild(n);
|
|
} else {
|
|
for (var i = 0, first = true; i < line.widgets.length; ++i) {
|
|
var widget = line.widgets[i], isFirst = false;
|
|
if (!widget.above) { isFirst = first; first = false; }
|
|
if (widget.node == n.firstChild) {
|
|
positionLineWidget(widget, n, reuse, dims);
|
|
++widgetsSeen;
|
|
if (isFirst) reuse.insertBefore(lineElement, n);
|
|
break;
|
|
}
|
|
}
|
|
if (i == line.widgets.length) { isOk = false; break; }
|
|
}
|
|
}
|
|
if (isOk && widgetsSeen == line.widgets.length) {
|
|
wrap = reuse;
|
|
reuse.className = line.wrapClass || "";
|
|
}
|
|
}
|
|
if (!wrap) {
|
|
wrap = elt("div", null, line.wrapClass, "position: relative");
|
|
wrap.appendChild(lineElement);
|
|
}
|
|
// Kludge to make sure the styled element lies behind the selection (by z-index)
|
|
if (line.bgClass)
|
|
wrap.insertBefore(elt("div", "\u00a0", line.bgClass + " CodeMirror-linebackground"), wrap.firstChild);
|
|
if (cm.options.lineNumbers || markers) {
|
|
var gutterWrap = wrap.insertBefore(elt("div", null, null, "position: absolute; left: " +
|
|
(cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"),
|
|
wrap.firstChild);
|
|
if (cm.options.fixedGutter) (wrap.alignable || (wrap.alignable = [])).push(gutterWrap);
|
|
if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"]))
|
|
wrap.lineNumber = gutterWrap.appendChild(
|
|
elt("div", lineNumberFor(cm.options, lineNo),
|
|
"CodeMirror-linenumber CodeMirror-gutter-elt",
|
|
"left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: "
|
|
+ display.lineNumInnerWidth + "px"));
|
|
if (markers)
|
|
for (var k = 0; k < cm.options.gutters.length; ++k) {
|
|
var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id];
|
|
if (found)
|
|
gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " +
|
|
dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px"));
|
|
}
|
|
}
|
|
if (ie_lt8) wrap.style.zIndex = 2;
|
|
if (line.widgets && wrap != reuse) for (var i = 0, ws = line.widgets; i < ws.length; ++i) {
|
|
var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget");
|
|
positionLineWidget(widget, node, wrap, dims);
|
|
if (widget.above)
|
|
wrap.insertBefore(node, cm.options.lineNumbers && line.height != 0 ? gutterWrap : lineElement);
|
|
else
|
|
wrap.appendChild(node);
|
|
signalLater(widget, "redraw");
|
|
}
|
|
return wrap;
|
|
}
|
|
|
|
function positionLineWidget(widget, node, wrap, dims) {
|
|
if (widget.noHScroll) {
|
|
(wrap.alignable || (wrap.alignable = [])).push(node);
|
|
var width = dims.wrapperWidth;
|
|
node.style.left = dims.fixedPos + "px";
|
|
if (!widget.coverGutter) {
|
|
width -= dims.gutterTotalWidth;
|
|
node.style.paddingLeft = dims.gutterTotalWidth + "px";
|
|
}
|
|
node.style.width = width + "px";
|
|
}
|
|
if (widget.coverGutter) {
|
|
node.style.zIndex = 5;
|
|
node.style.position = "relative";
|
|
if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px";
|
|
}
|
|
}
|
|
|
|
// SELECTION / CURSOR
|
|
|
|
function updateSelection(cm) {
|
|
var display = cm.display;
|
|
var collapsed = posEq(cm.doc.sel.from, cm.doc.sel.to);
|
|
if (collapsed || cm.options.showCursorWhenSelecting)
|
|
updateSelectionCursor(cm);
|
|
else
|
|
display.cursor.style.display = display.otherCursor.style.display = "none";
|
|
if (!collapsed)
|
|
updateSelectionRange(cm);
|
|
else
|
|
display.selectionDiv.style.display = "none";
|
|
|
|
// Move the hidden textarea near the cursor to prevent scrolling artifacts
|
|
var headPos = cursorCoords(cm, cm.doc.sel.head, "div");
|
|
var wrapOff = getRect(display.wrapper), lineOff = getRect(display.lineDiv);
|
|
display.inputDiv.style.top = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
|
|
headPos.top + lineOff.top - wrapOff.top)) + "px";
|
|
display.inputDiv.style.left = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
|
|
headPos.left + lineOff.left - wrapOff.left)) + "px";
|
|
}
|
|
|
|
// No selection, plain cursor
|
|
function updateSelectionCursor(cm) {
|
|
var display = cm.display, pos = cursorCoords(cm, cm.doc.sel.head, "div");
|
|
display.cursor.style.left = pos.left + "px";
|
|
display.cursor.style.top = pos.top + "px";
|
|
display.cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px";
|
|
display.cursor.style.display = "";
|
|
|
|
if (pos.other) {
|
|
display.otherCursor.style.display = "";
|
|
display.otherCursor.style.left = pos.other.left + "px";
|
|
display.otherCursor.style.top = pos.other.top + "px";
|
|
display.otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px";
|
|
} else { display.otherCursor.style.display = "none"; }
|
|
}
|
|
|
|
// Highlight selection
|
|
function updateSelectionRange(cm) {
|
|
var display = cm.display, doc = cm.doc, sel = cm.doc.sel;
|
|
var fragment = document.createDocumentFragment();
|
|
var clientWidth = display.lineSpace.offsetWidth, pl = paddingLeft(cm.display);
|
|
|
|
function add(left, top, width, bottom) {
|
|
if (top < 0) top = 0;
|
|
fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left +
|
|
"px; top: " + top + "px; width: " + (width == null ? clientWidth - left : width) +
|
|
"px; height: " + (bottom - top) + "px"));
|
|
}
|
|
|
|
function drawForLine(line, fromArg, toArg, retTop) {
|
|
var lineObj = getLine(doc, line);
|
|
var lineLen = lineObj.text.length, rVal = retTop ? Infinity : -Infinity;
|
|
function coords(ch) {
|
|
return charCoords(cm, Pos(line, ch), "div", lineObj);
|
|
}
|
|
|
|
iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) {
|
|
var leftPos = coords(dir == "rtl" ? to - 1 : from);
|
|
var rightPos = coords(dir == "rtl" ? from : to - 1);
|
|
var left = leftPos.left, right = rightPos.right;
|
|
if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part
|
|
add(left, leftPos.top, null, leftPos.bottom);
|
|
left = pl;
|
|
if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top);
|
|
}
|
|
if (toArg == null && to == lineLen) right = clientWidth;
|
|
if (fromArg == null && from == 0) left = pl;
|
|
rVal = retTop ? Math.min(rightPos.top, rVal) : Math.max(rightPos.bottom, rVal);
|
|
if (left < pl + 1) left = pl;
|
|
add(left, rightPos.top, right - left, rightPos.bottom);
|
|
});
|
|
return rVal;
|
|
}
|
|
|
|
if (sel.from.line == sel.to.line) {
|
|
drawForLine(sel.from.line, sel.from.ch, sel.to.ch);
|
|
} else {
|
|
var fromObj = getLine(doc, sel.from.line);
|
|
var cur = fromObj, merged, path = [sel.from.line, sel.from.ch], singleLine;
|
|
while (merged = collapsedSpanAtEnd(cur)) {
|
|
var found = merged.find();
|
|
path.push(found.from.ch, found.to.line, found.to.ch);
|
|
if (found.to.line == sel.to.line) {
|
|
path.push(sel.to.ch);
|
|
singleLine = true;
|
|
break;
|
|
}
|
|
cur = getLine(doc, found.to.line);
|
|
}
|
|
|
|
// This is a single, merged line
|
|
if (singleLine) {
|
|
for (var i = 0; i < path.length; i += 3)
|
|
drawForLine(path[i], path[i+1], path[i+2]);
|
|
} else {
|
|
var middleTop, middleBot, toObj = getLine(doc, sel.to.line);
|
|
if (sel.from.ch)
|
|
// Draw the first line of selection.
|
|
middleTop = drawForLine(sel.from.line, sel.from.ch, null, false);
|
|
else
|
|
// Simply include it in the middle block.
|
|
middleTop = heightAtLine(cm, fromObj) - display.viewOffset;
|
|
|
|
if (!sel.to.ch)
|
|
middleBot = heightAtLine(cm, toObj) - display.viewOffset;
|
|
else
|
|
middleBot = drawForLine(sel.to.line, collapsedSpanAtStart(toObj) ? null : 0, sel.to.ch, true);
|
|
|
|
if (middleTop < middleBot) add(pl, middleTop, null, middleBot);
|
|
}
|
|
}
|
|
|
|
removeChildrenAndAdd(display.selectionDiv, fragment);
|
|
display.selectionDiv.style.display = "";
|
|
}
|
|
|
|
// Cursor-blinking
|
|
function restartBlink(cm) {
|
|
var display = cm.display;
|
|
clearInterval(display.blinker);
|
|
var on = true;
|
|
display.cursor.style.visibility = display.otherCursor.style.visibility = "";
|
|
display.blinker = setInterval(function() {
|
|
if (!display.cursor.offsetHeight) return;
|
|
display.cursor.style.visibility = display.otherCursor.style.visibility = (on = !on) ? "" : "hidden";
|
|
}, cm.options.cursorBlinkRate);
|
|
}
|
|
|
|
// HIGHLIGHT WORKER
|
|
|
|
function startWorker(cm, time) {
|
|
if (cm.doc.mode.startState && cm.doc.frontier < cm.display.showingTo)
|
|
cm.state.highlight.set(time, bind(highlightWorker, cm));
|
|
}
|
|
|
|
function highlightWorker(cm) {
|
|
var doc = cm.doc;
|
|
if (doc.frontier < doc.first) doc.frontier = doc.first;
|
|
if (doc.frontier >= cm.display.showingTo) return;
|
|
var end = +new Date + cm.options.workTime;
|
|
var state = copyState(doc.mode, getStateBefore(cm, doc.frontier));
|
|
var changed = [], prevChange;
|
|
doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.showingTo + 500), function(line) {
|
|
if (doc.frontier >= cm.display.showingFrom) { // Visible
|
|
var oldStyles = line.styles;
|
|
line.styles = highlightLine(cm, line, state);
|
|
var ischange = !oldStyles || oldStyles.length != line.styles.length;
|
|
for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i];
|
|
if (ischange) {
|
|
if (prevChange && prevChange.end == doc.frontier) prevChange.end++;
|
|
else changed.push(prevChange = {start: doc.frontier, end: doc.frontier + 1});
|
|
}
|
|
line.stateAfter = copyState(doc.mode, state);
|
|
} else {
|
|
processLine(cm, line, state);
|
|
line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null;
|
|
}
|
|
++doc.frontier;
|
|
if (+new Date > end) {
|
|
startWorker(cm, cm.options.workDelay);
|
|
return true;
|
|
}
|
|
});
|
|
if (changed.length)
|
|
operation(cm, function() {
|
|
for (var i = 0; i < changed.length; ++i)
|
|
regChange(this, changed[i].start, changed[i].end);
|
|
})();
|
|
}
|
|
|
|
// Finds the line to start with when starting a parse. Tries to
|
|
// find a line with a stateAfter, so that it can start with a
|
|
// valid state. If that fails, it returns the line with the
|
|
// smallest indentation, which tends to need the least context to
|
|
// parse correctly.
|
|
function findStartLine(cm, n) {
|
|
var minindent, minline, doc = cm.doc;
|
|
for (var search = n, lim = n - 100; search > lim; --search) {
|
|
if (search <= doc.first) return doc.first;
|
|
var line = getLine(doc, search - 1);
|
|
if (line.stateAfter) return search;
|
|
var indented = countColumn(line.text, null, cm.options.tabSize);
|
|
if (minline == null || minindent > indented) {
|
|
minline = search - 1;
|
|
minindent = indented;
|
|
}
|
|
}
|
|
return minline;
|
|
}
|
|
|
|
function getStateBefore(cm, n) {
|
|
var doc = cm.doc, display = cm.display;
|
|
if (!doc.mode.startState) return true;
|
|
var pos = findStartLine(cm, n), state = pos > doc.first && getLine(doc, pos-1).stateAfter;
|
|
if (!state) state = startState(doc.mode);
|
|
else state = copyState(doc.mode, state);
|
|
doc.iter(pos, n, function(line) {
|
|
processLine(cm, line, state);
|
|
var save = pos == n - 1 || pos % 5 == 0 || pos >= display.showingFrom && pos < display.showingTo;
|
|
line.stateAfter = save ? copyState(doc.mode, state) : null;
|
|
++pos;
|
|
});
|
|
return state;
|
|
}
|
|
|
|
// POSITION MEASUREMENT
|
|
|
|
function paddingTop(display) {return display.lineSpace.offsetTop;}
|
|
function paddingLeft(display) {
|
|
var e = removeChildrenAndAdd(display.measure, elt("pre", null, null, "text-align: left")).appendChild(elt("span", "x"));
|
|
return e.offsetLeft;
|
|
}
|
|
|
|
function measureChar(cm, line, ch, data) {
|
|
var dir = -1;
|
|
data = data || measureLine(cm, line);
|
|
|
|
for (var pos = ch;; pos += dir) {
|
|
var r = data[pos];
|
|
if (r) break;
|
|
if (dir < 0 && pos == 0) dir = 1;
|
|
}
|
|
return {left: pos < ch ? r.right : r.left,
|
|
right: pos > ch ? r.left : r.right,
|
|
top: r.top, bottom: r.bottom};
|
|
}
|
|
|
|
function measureLine(cm, line) {
|
|
// First look in the cache
|
|
var display = cm.display, cache = cm.display.measureLineCache;
|
|
for (var i = 0; i < cache.length; ++i) {
|
|
var memo = cache[i];
|
|
if (memo.text == line.text && memo.markedSpans == line.markedSpans &&
|
|
display.scroller.clientWidth == memo.width &&
|
|
memo.classes == line.textClass + "|" + line.bgClass + "|" + line.wrapClass)
|
|
return memo.measure;
|
|
}
|
|
|
|
var measure = measureLineInner(cm, line);
|
|
// Store result in the cache
|
|
var memo = {text: line.text, width: display.scroller.clientWidth,
|
|
markedSpans: line.markedSpans, measure: measure,
|
|
classes: line.textClass + "|" + line.bgClass + "|" + line.wrapClass};
|
|
if (cache.length == 16) cache[++display.measureLineCachePos % 16] = memo;
|
|
else cache.push(memo);
|
|
return measure;
|
|
}
|
|
|
|
function measureLineInner(cm, line) {
|
|
var display = cm.display, measure = emptyArray(line.text.length);
|
|
var pre = lineContent(cm, line, measure);
|
|
|
|
// IE does not cache element positions of inline elements between
|
|
// calls to getBoundingClientRect. This makes the loop below,
|
|
// which gathers the positions of all the characters on the line,
|
|
// do an amount of layout work quadratic to the number of
|
|
// characters. When line wrapping is off, we try to improve things
|
|
// by first subdividing the line into a bunch of inline blocks, so
|
|
// that IE can reuse most of the layout information from caches
|
|
// for those blocks. This does interfere with line wrapping, so it
|
|
// doesn't work when wrapping is on, but in that case the
|
|
// situation is slightly better, since IE does cache line-wrapping
|
|
// information and only recomputes per-line.
|
|
if (ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) {
|
|
var fragment = document.createDocumentFragment();
|
|
var chunk = 10, n = pre.childNodes.length;
|
|
for (var i = 0, chunks = Math.ceil(n / chunk); i < chunks; ++i) {
|
|
var wrap = elt("div", null, null, "display: inline-block");
|
|
for (var j = 0; j < chunk && n; ++j) {
|
|
wrap.appendChild(pre.firstChild);
|
|
--n;
|
|
}
|
|
fragment.appendChild(wrap);
|
|
}
|
|
pre.appendChild(fragment);
|
|
}
|
|
|
|
removeChildrenAndAdd(display.measure, pre);
|
|
|
|
var outer = getRect(display.lineDiv);
|
|
var vranges = [], data = emptyArray(line.text.length), maxBot = pre.offsetHeight;
|
|
// Work around an IE7/8 bug where it will sometimes have randomly
|
|
// replaced our pre with a clone at this point.
|
|
if (ie_lt9 && display.measure.first != pre)
|
|
removeChildrenAndAdd(display.measure, pre);
|
|
|
|
for (var i = 0, cur; i < measure.length; ++i) if (cur = measure[i]) {
|
|
var size = getRect(cur);
|
|
var top = Math.max(0, size.top - outer.top), bot = Math.min(size.bottom - outer.top, maxBot);
|
|
for (var j = 0; j < vranges.length; j += 2) {
|
|
var rtop = vranges[j], rbot = vranges[j+1];
|
|
if (rtop > bot || rbot < top) continue;
|
|
if (rtop <= top && rbot >= bot ||
|
|
top <= rtop && bot >= rbot ||
|
|
Math.min(bot, rbot) - Math.max(top, rtop) >= (bot - top) >> 1) {
|
|
vranges[j] = Math.min(top, rtop);
|
|
vranges[j+1] = Math.max(bot, rbot);
|
|
break;
|
|
}
|
|
}
|
|
if (j == vranges.length) vranges.push(top, bot);
|
|
var right = size.right;
|
|
if (cur.measureRight) right = getRect(cur.measureRight).left;
|
|
data[i] = {left: size.left - outer.left, right: right - outer.left, top: j};
|
|
}
|
|
for (var i = 0, cur; i < data.length; ++i) if (cur = data[i]) {
|
|
var vr = cur.top;
|
|
cur.top = vranges[vr]; cur.bottom = vranges[vr+1];
|
|
}
|
|
if (!cm.options.lineWrapping) {
|
|
var last = pre.lastChild;
|
|
if (last.nodeType == 3) last = pre.appendChild(elt("span", "\u200b"));
|
|
data.width = getRect(last).right - outer.left;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
function clearCaches(cm) {
|
|
cm.display.measureLineCache.length = cm.display.measureLineCachePos = 0;
|
|
cm.display.cachedCharWidth = cm.display.cachedTextHeight = null;
|
|
cm.display.maxLineChanged = true;
|
|
cm.display.lineNumChars = null;
|
|
}
|
|
|
|
// Context is one of "line", "div" (display.lineDiv), "local"/null (editor), or "page"
|
|
function intoCoordSystem(cm, lineObj, rect, context) {
|
|
if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) {
|
|
var size = widgetHeight(lineObj.widgets[i]);
|
|
rect.top += size; rect.bottom += size;
|
|
}
|
|
if (context == "line") return rect;
|
|
if (!context) context = "local";
|
|
var yOff = heightAtLine(cm, lineObj);
|
|
if (context != "local") yOff -= cm.display.viewOffset;
|
|
if (context == "page") {
|
|
var lOff = getRect(cm.display.lineSpace);
|
|
yOff += lOff.top + (window.pageYOffset || (document.documentElement || document.body).scrollTop);
|
|
var xOff = lOff.left + (window.pageXOffset || (document.documentElement || document.body).scrollLeft);
|
|
rect.left += xOff; rect.right += xOff;
|
|
}
|
|
rect.top += yOff; rect.bottom += yOff;
|
|
return rect;
|
|
}
|
|
|
|
function charCoords(cm, pos, context, lineObj) {
|
|
if (!lineObj) lineObj = getLine(cm.doc, pos.line);
|
|
return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch), context);
|
|
}
|
|
|
|
function cursorCoords(cm, pos, context, lineObj, measurement) {
|
|
lineObj = lineObj || getLine(cm.doc, pos.line);
|
|
if (!measurement) measurement = measureLine(cm, lineObj);
|
|
function get(ch, right) {
|
|
var m = measureChar(cm, lineObj, ch, measurement);
|
|
if (right) m.left = m.right; else m.right = m.left;
|
|
return intoCoordSystem(cm, lineObj, m, context);
|
|
}
|
|
var order = getOrder(lineObj), ch = pos.ch;
|
|
if (!order) return get(ch);
|
|
var main, other, linedir = order[0].level;
|
|
for (var i = 0; i < order.length; ++i) {
|
|
var part = order[i], rtl = part.level % 2, nb, here;
|
|
if (part.from < ch && part.to > ch) return get(ch, rtl);
|
|
var left = rtl ? part.to : part.from, right = rtl ? part.from : part.to;
|
|
if (left == ch) {
|
|
// IE returns bogus offsets and widths for edges where the
|
|
// direction flips, but only for the side with the lower
|
|
// level. So we try to use the side with the higher level.
|
|
if (i && part.level < (nb = order[i-1]).level) here = get(nb.level % 2 ? nb.from : nb.to - 1, true);
|
|
else here = get(rtl && part.from != part.to ? ch - 1 : ch);
|
|
if (rtl == linedir) main = here; else other = here;
|
|
} else if (right == ch) {
|
|
var nb = i < order.length - 1 && order[i+1];
|
|
if (!rtl && nb && nb.from == nb.to) continue;
|
|
if (nb && part.level < nb.level) here = get(nb.level % 2 ? nb.to - 1 : nb.from);
|
|
else here = get(rtl ? ch : ch - 1, true);
|
|
if (rtl == linedir) main = here; else other = here;
|
|
}
|
|
}
|
|
if (linedir && !ch) other = get(order[0].to - 1);
|
|
if (!main) return other;
|
|
if (other) main.other = other;
|
|
return main;
|
|
}
|
|
|
|
function PosMaybeOutside(line, ch, outside) {
|
|
var pos = new Pos(line, ch);
|
|
if (outside) pos.outside = true;
|
|
return pos;
|
|
}
|
|
|
|
// Coords must be lineSpace-local
|
|
function coordsChar(cm, x, y) {
|
|
var doc = cm.doc;
|
|
y += cm.display.viewOffset;
|
|
if (y < 0) return PosMaybeOutside(doc.first, 0, true);
|
|
var lineNo = lineAtHeight(doc, y), last = doc.first + doc.size - 1;
|
|
if (lineNo > last)
|
|
return PosMaybeOutside(doc.first + doc.size - 1, getLine(doc, last).text.length, true);
|
|
if (x < 0) x = 0;
|
|
|
|
for (;;) {
|
|
var lineObj = getLine(doc, lineNo);
|
|
var found = coordsCharInner(cm, lineObj, lineNo, x, y);
|
|
var merged = collapsedSpanAtEnd(lineObj);
|
|
var mergedPos = merged && merged.find();
|
|
if (merged && found.ch >= mergedPos.from.ch)
|
|
lineNo = mergedPos.to.line;
|
|
else
|
|
return found;
|
|
}
|
|
}
|
|
|
|
function coordsCharInner(cm, lineObj, lineNo, x, y) {
|
|
var innerOff = y - heightAtLine(cm, lineObj);
|
|
var wrongLine = false, cWidth = cm.display.wrapper.clientWidth;
|
|
var measurement = measureLine(cm, lineObj);
|
|
|
|
function getX(ch) {
|
|
var sp = cursorCoords(cm, Pos(lineNo, ch), "line",
|
|
lineObj, measurement);
|
|
wrongLine = true;
|
|
if (innerOff > sp.bottom) return Math.max(0, sp.left - cWidth);
|
|
else if (innerOff < sp.top) return sp.left + cWidth;
|
|
else wrongLine = false;
|
|
return sp.left;
|
|
}
|
|
|
|
var bidi = getOrder(lineObj), dist = lineObj.text.length;
|
|
var from = lineLeft(lineObj), to = lineRight(lineObj);
|
|
var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine;
|
|
|
|
if (x > toX) return PosMaybeOutside(lineNo, to, toOutside);
|
|
// Do a binary search between these bounds.
|
|
for (;;) {
|
|
if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
|
|
var after = x - fromX < toX - x, ch = after ? from : to;
|
|
while (isExtendingChar.test(lineObj.text.charAt(ch))) ++ch;
|
|
var pos = PosMaybeOutside(lineNo, ch, after ? fromOutside : toOutside);
|
|
pos.after = after;
|
|
return pos;
|
|
}
|
|
var step = Math.ceil(dist / 2), middle = from + step;
|
|
if (bidi) {
|
|
middle = from;
|
|
for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1);
|
|
}
|
|
var middleX = getX(middle);
|
|
if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist -= step;}
|
|
else {from = middle; fromX = middleX; fromOutside = wrongLine; dist = step;}
|
|
}
|
|
}
|
|
|
|
var measureText;
|
|
function textHeight(display) {
|
|
if (display.cachedTextHeight != null) return display.cachedTextHeight;
|
|
if (measureText == null) {
|
|
measureText = elt("pre");
|
|
// Measure a bunch of lines, for browsers that compute
|
|
// fractional heights.
|
|
for (var i = 0; i < 49; ++i) {
|
|
measureText.appendChild(document.createTextNode("x"));
|
|
measureText.appendChild(elt("br"));
|
|
}
|
|
measureText.appendChild(document.createTextNode("x"));
|
|
}
|
|
removeChildrenAndAdd(display.measure, measureText);
|
|
var height = measureText.offsetHeight / 50;
|
|
if (height > 3) display.cachedTextHeight = height;
|
|
removeChildren(display.measure);
|
|
return height || 1;
|
|
}
|
|
|
|
function charWidth(display) {
|
|
if (display.cachedCharWidth != null) return display.cachedCharWidth;
|
|
var anchor = elt("span", "x");
|
|
var pre = elt("pre", [anchor]);
|
|
removeChildrenAndAdd(display.measure, pre);
|
|
var width = anchor.offsetWidth;
|
|
if (width > 2) display.cachedCharWidth = width;
|
|
return width || 10;
|
|
}
|
|
|
|
// OPERATIONS
|
|
|
|
// Operations are used to wrap changes in such a way that each
|
|
// change won't have to update the cursor and display (which would
|
|
// be awkward, slow, and error-prone), but instead updates are
|
|
// batched and then all combined and executed at once.
|
|
|
|
var nextOpId = 0;
|
|
function startOperation(cm) {
|
|
cm.curOp = {
|
|
// An array of ranges of lines that have to be updated. See
|
|
// updateDisplay.
|
|
changes: [],
|
|
updateInput: null,
|
|
userSelChange: null,
|
|
textChanged: null,
|
|
selectionChanged: false,
|
|
updateMaxLine: false,
|
|
updateScrollPos: false,
|
|
id: ++nextOpId
|
|
};
|
|
if (!delayedCallbackDepth++) delayedCallbacks = [];
|
|
}
|
|
|
|
function endOperation(cm) {
|
|
var op = cm.curOp, doc = cm.doc, display = cm.display;
|
|
cm.curOp = null;
|
|
|
|
if (op.updateMaxLine) computeMaxLength(cm);
|
|
if (display.maxLineChanged && !cm.options.lineWrapping) {
|
|
var width = measureLine(cm, display.maxLine).width;
|
|
display.sizer.style.minWidth = Math.max(0, width + 3 + scrollerCutOff) + "px";
|
|
display.maxLineChanged = false;
|
|
var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + display.sizer.offsetWidth - display.scroller.clientWidth);
|
|
if (maxScrollLeft < doc.scrollLeft && !op.updateScrollPos)
|
|
setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true);
|
|
}
|
|
var newScrollPos, updated;
|
|
if (op.updateScrollPos) {
|
|
newScrollPos = op.updateScrollPos;
|
|
} else if (op.selectionChanged && display.scroller.clientHeight) { // don't rescroll if not visible
|
|
var coords = cursorCoords(cm, doc.sel.head);
|
|
newScrollPos = calculateScrollPos(cm, coords.left, coords.top, coords.left, coords.bottom);
|
|
}
|
|
if (op.changes.length || newScrollPos && newScrollPos.scrollTop != null)
|
|
updated = updateDisplay(cm, op.changes, newScrollPos && newScrollPos.scrollTop);
|
|
if (!updated && op.selectionChanged) updateSelection(cm);
|
|
if (op.updateScrollPos) {
|
|
display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = newScrollPos.scrollTop;
|
|
display.scroller.scrollLeft = display.scrollbarH.scrollLeft = doc.scrollLeft = newScrollPos.scrollLeft;
|
|
alignHorizontally(cm);
|
|
} else if (newScrollPos) {
|
|
scrollCursorIntoView(cm);
|
|
}
|
|
if (op.selectionChanged) restartBlink(cm);
|
|
|
|
if (cm.state.focused && op.updateInput)
|
|
resetInput(cm, op.userSelChange);
|
|
|
|
var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers;
|
|
if (hidden) for (var i = 0; i < hidden.length; ++i)
|
|
if (!hidden[i].lines.length) signal(hidden[i], "hide");
|
|
if (unhidden) for (var i = 0; i < unhidden.length; ++i)
|
|
if (unhidden[i].lines.length) signal(unhidden[i], "unhide");
|
|
|
|
var delayed;
|
|
if (!--delayedCallbackDepth) {
|
|
delayed = delayedCallbacks;
|
|
delayedCallbacks = null;
|
|
}
|
|
if (op.textChanged)
|
|
signal(cm, "change", cm, op.textChanged);
|
|
if (op.selectionChanged) signal(cm, "cursorActivity", cm);
|
|
if (delayed) for (var i = 0; i < delayed.length; ++i) delayed[i]();
|
|
}
|
|
|
|
// Wraps a function in an operation. Returns the wrapped function.
|
|
function operation(cm1, f) {
|
|
return function() {
|
|
var cm = cm1 || this, withOp = !cm.curOp;
|
|
if (withOp) startOperation(cm);
|
|
try { var result = f.apply(cm, arguments); }
|
|
finally { if (withOp) endOperation(cm); }
|
|
return result;
|
|
};
|
|
}
|
|
function docOperation(f) {
|
|
return function() {
|
|
var withOp = this.cm && !this.cm.curOp, result;
|
|
if (withOp) startOperation(this.cm);
|
|
try { result = f.apply(this, arguments); }
|
|
finally { if (withOp) endOperation(this.cm); }
|
|
return result;
|
|
};
|
|
}
|
|
function runInOp(cm, f) {
|
|
var withOp = !cm.curOp, result;
|
|
if (withOp) startOperation(cm);
|
|
try { result = f(); }
|
|
finally { if (withOp) endOperation(cm); }
|
|
return result;
|
|
}
|
|
|
|
function regChange(cm, from, to, lendiff) {
|
|
if (from == null) from = cm.doc.first;
|
|
if (to == null) to = cm.doc.first + cm.doc.size;
|
|
cm.curOp.changes.push({from: from, to: to, diff: lendiff});
|
|
}
|
|
|
|
// INPUT HANDLING
|
|
|
|
function slowPoll(cm) {
|
|
if (cm.display.pollingFast) return;
|
|
cm.display.poll.set(cm.options.pollInterval, function() {
|
|
readInput(cm);
|
|
if (cm.state.focused) slowPoll(cm);
|
|
});
|
|
}
|
|
|
|
function fastPoll(cm) {
|
|
var missed = false;
|
|
cm.display.pollingFast = true;
|
|
function p() {
|
|
var changed = readInput(cm);
|
|
if (!changed && !missed) {missed = true; cm.display.poll.set(60, p);}
|
|
else {cm.display.pollingFast = false; slowPoll(cm);}
|
|
}
|
|
cm.display.poll.set(20, p);
|
|
}
|
|
|
|
// prevInput is a hack to work with IME. If we reset the textarea
|
|
// on every change, that breaks IME. So we look for changes
|
|
// compared to the previous content instead. (Modern browsers have
|
|
// events that indicate IME taking place, but these are not widely
|
|
// supported or compatible enough yet to rely on.)
|
|
function readInput(cm) {
|
|
var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel;
|
|
if (!cm.state.focused || hasSelection(input) || isReadOnly(cm)) return false;
|
|
var text = input.value;
|
|
if (text == prevInput && posEq(sel.from, sel.to)) return false;
|
|
// IE enjoys randomly deselecting our input's text when
|
|
// re-focusing. If the selection is gone but the cursor is at the
|
|
// start of the input, that's probably what happened.
|
|
if (ie && text && input.selectionStart === 0) {
|
|
resetInput(cm, true);
|
|
return false;
|
|
}
|
|
var withOp = !cm.curOp;
|
|
if (withOp) startOperation(cm);
|
|
sel.shift = false;
|
|
var same = 0, l = Math.min(prevInput.length, text.length);
|
|
while (same < l && prevInput[same] == text[same]) ++same;
|
|
var from = sel.from, to = sel.to;
|
|
if (same < prevInput.length)
|
|
from = Pos(from.line, from.ch - (prevInput.length - same));
|
|
else if (cm.state.overwrite && posEq(from, to) && !cm.state.pasteIncoming)
|
|
to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + (text.length - same)));
|
|
var updateInput = cm.curOp.updateInput;
|
|
makeChange(cm.doc, {from: from, to: to, text: splitLines(text.slice(same)),
|
|
origin: cm.state.pasteIncoming ? "paste" : "+input"}, "end");
|
|
|
|
cm.curOp.updateInput = updateInput;
|
|
if (text.length > 1000) input.value = cm.display.prevInput = "";
|
|
else cm.display.prevInput = text;
|
|
if (withOp) endOperation(cm);
|
|
cm.state.pasteIncoming = false;
|
|
return true;
|
|
}
|
|
|
|
function resetInput(cm, user) {
|
|
var minimal, selected, doc = cm.doc;
|
|
if (!posEq(doc.sel.from, doc.sel.to)) {
|
|
cm.display.prevInput = "";
|
|
minimal = hasCopyEvent &&
|
|
(doc.sel.to.line - doc.sel.from.line > 100 || (selected = cm.getSelection()).length > 1000);
|
|
if (minimal) cm.display.input.value = "-";
|
|
else cm.display.input.value = selected || cm.getSelection();
|
|
if (cm.state.focused) selectInput(cm.display.input);
|
|
} else if (user) cm.display.prevInput = cm.display.input.value = "";
|
|
cm.display.inaccurateSelection = minimal;
|
|
}
|
|
|
|
function focusInput(cm) {
|
|
if (cm.options.readOnly != "nocursor" && (!mobile || document.activeElement != cm.display.input))
|
|
cm.display.input.focus();
|
|
}
|
|
|
|
function isReadOnly(cm) {
|
|
return cm.options.readOnly || cm.doc.cantEdit;
|
|
}
|
|
|
|
// EVENT HANDLERS
|
|
|
|
function registerEventHandlers(cm) {
|
|
var d = cm.display;
|
|
on(d.scroller, "mousedown", operation(cm, onMouseDown));
|
|
on(d.scroller, "dblclick", operation(cm, e_preventDefault));
|
|
on(d.lineSpace, "selectstart", function(e) {
|
|
if (!eventInWidget(d, e)) e_preventDefault(e);
|
|
});
|
|
// Gecko browsers fire contextmenu *after* opening the menu, at
|
|
// which point we can't mess with it anymore. Context menu is
|
|
// handled in onMouseDown for Gecko.
|
|
if (!captureMiddleClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);});
|
|
|
|
on(d.scroller, "scroll", function() {
|
|
setScrollTop(cm, d.scroller.scrollTop);
|
|
setScrollLeft(cm, d.scroller.scrollLeft, true);
|
|
signal(cm, "scroll", cm);
|
|
});
|
|
on(d.scrollbarV, "scroll", function() {
|
|
setScrollTop(cm, d.scrollbarV.scrollTop);
|
|
});
|
|
on(d.scrollbarH, "scroll", function() {
|
|
setScrollLeft(cm, d.scrollbarH.scrollLeft);
|
|
});
|
|
|
|
on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);});
|
|
on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);});
|
|
|
|
function reFocus() { if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); }
|
|
on(d.scrollbarH, "mousedown", reFocus);
|
|
on(d.scrollbarV, "mousedown", reFocus);
|
|
// Prevent wrapper from ever scrolling
|
|
on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; });
|
|
|
|
function onResize() {
|
|
// Might be a text scaling operation, clear size caches.
|
|
d.cachedCharWidth = d.cachedTextHeight = null;
|
|
clearCaches(cm);
|
|
runInOp(cm, bind(regChange, cm));
|
|
}
|
|
on(window, "resize", onResize);
|
|
// Above handler holds on to the editor and its data structures.
|
|
// Here we poll to unregister it when the editor is no longer in
|
|
// the document, so that it can be garbage-collected.
|
|
function unregister() {
|
|
for (var p = d.wrapper.parentNode; p && p != document.body; p = p.parentNode) {}
|
|
if (p) setTimeout(unregister, 5000);
|
|
else off(window, "resize", onResize);
|
|
}
|
|
setTimeout(unregister, 5000);
|
|
|
|
on(d.input, "keyup", operation(cm, function(e) {
|
|
if (cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
|
|
if (e.keyCode == 16) cm.doc.sel.shift = false;
|
|
}));
|
|
on(d.input, "input", bind(fastPoll, cm));
|
|
on(d.input, "keydown", operation(cm, onKeyDown));
|
|
on(d.input, "keypress", operation(cm, onKeyPress));
|
|
on(d.input, "focus", bind(onFocus, cm));
|
|
on(d.input, "blur", bind(onBlur, cm));
|
|
|
|
function drag_(e) {
|
|
if (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))) return;
|
|
e_stop(e);
|
|
}
|
|
if (cm.options.dragDrop) {
|
|
on(d.scroller, "dragstart", function(e){onDragStart(cm, e);});
|
|
on(d.scroller, "dragenter", drag_);
|
|
on(d.scroller, "dragover", drag_);
|
|
on(d.scroller, "drop", operation(cm, onDrop));
|
|
}
|
|
on(d.scroller, "paste", function(e){
|
|
if (eventInWidget(d, e)) return;
|
|
focusInput(cm);
|
|
fastPoll(cm);
|
|
});
|
|
on(d.input, "paste", function() {
|
|
cm.state.pasteIncoming = true;
|
|
fastPoll(cm);
|
|
});
|
|
|
|
function prepareCopy() {
|
|
if (d.inaccurateSelection) {
|
|
d.prevInput = "";
|
|
d.inaccurateSelection = false;
|
|
d.input.value = cm.getSelection();
|
|
selectInput(d.input);
|
|
}
|
|
}
|
|
on(d.input, "cut", prepareCopy);
|
|
on(d.input, "copy", prepareCopy);
|
|
|
|
// Needed to handle Tab key in KHTML
|
|
if (khtml) on(d.sizer, "mouseup", function() {
|
|
if (document.activeElement == d.input) d.input.blur();
|
|
focusInput(cm);
|
|
});
|
|
}
|
|
|
|
function eventInWidget(display, e) {
|
|
for (var n = e_target(e); n != display.wrapper; n = n.parentNode) {
|
|
if (!n) return true;
|
|
if (/\bCodeMirror-(?:line)?widget\b/.test(n.className) ||
|
|
n.parentNode == display.sizer && n != display.mover) return true;
|
|
}
|
|
}
|
|
|
|
function posFromMouse(cm, e, liberal) {
|
|
var display = cm.display;
|
|
if (!liberal) {
|
|
var target = e_target(e);
|
|
if (target == display.scrollbarH || target == display.scrollbarH.firstChild ||
|
|
target == display.scrollbarV || target == display.scrollbarV.firstChild ||
|
|
target == display.scrollbarFiller) return null;
|
|
}
|
|
var x, y, space = getRect(display.lineSpace);
|
|
// Fails unpredictably on IE[67] when mouse is dragged around quickly.
|
|
try { x = e.clientX; y = e.clientY; } catch (e) { return null; }
|
|
return coordsChar(cm, x - space.left, y - space.top);
|
|
}
|
|
|
|
var lastClick, lastDoubleClick;
|
|
function onMouseDown(e) {
|
|
var cm = this, display = cm.display, doc = cm.doc, sel = doc.sel;
|
|
sel.shift = e.shiftKey;
|
|
|
|
if (eventInWidget(display, e)) {
|
|
if (!webkit) {
|
|
display.scroller.draggable = false;
|
|
setTimeout(function(){display.scroller.draggable = true;}, 100);
|
|
}
|
|
return;
|
|
}
|
|
if (clickInGutter(cm, e)) return;
|
|
var start = posFromMouse(cm, e);
|
|
|
|
switch (e_button(e)) {
|
|
case 3:
|
|
if (captureMiddleClick) onContextMenu.call(cm, cm, e);
|
|
return;
|
|
case 2:
|
|
if (start) extendSelection(cm.doc, start);
|
|
setTimeout(bind(focusInput, cm), 20);
|
|
e_preventDefault(e);
|
|
return;
|
|
}
|
|
// For button 1, if it was clicked inside the editor
|
|
// (posFromMouse returning non-null), we have to adjust the
|
|
// selection.
|
|
if (!start) {if (e_target(e) == display.scroller) e_preventDefault(e); return;}
|
|
|
|
if (!cm.state.focused) onFocus(cm);
|
|
|
|
var now = +new Date, type = "single";
|
|
if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) {
|
|
type = "triple";
|
|
e_preventDefault(e);
|
|
setTimeout(bind(focusInput, cm), 20);
|
|
selectLine(cm, start.line);
|
|
} else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) {
|
|
type = "double";
|
|
lastDoubleClick = {time: now, pos: start};
|
|
e_preventDefault(e);
|
|
var word = findWordAt(getLine(doc, start.line).text, start);
|
|
extendSelection(cm.doc, word.from, word.to);
|
|
} else { lastClick = {time: now, pos: start}; }
|
|
|
|
var last = start;
|
|
if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && !posEq(sel.from, sel.to) &&
|
|
!posLess(start, sel.from) && !posLess(sel.to, start) && type == "single") {
|
|
var dragEnd = operation(cm, function(e2) {
|
|
if (webkit) display.scroller.draggable = false;
|
|
cm.state.draggingText = false;
|
|
off(document, "mouseup", dragEnd);
|
|
off(display.scroller, "drop", dragEnd);
|
|
if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
|
|
e_preventDefault(e2);
|
|
extendSelection(cm.doc, start);
|
|
focusInput(cm);
|
|
}
|
|
});
|
|
// Let the drag handler handle this.
|
|
if (webkit) display.scroller.draggable = true;
|
|
cm.state.draggingText = dragEnd;
|
|
// IE's approach to draggable
|
|
if (display.scroller.dragDrop) display.scroller.dragDrop();
|
|
on(document, "mouseup", dragEnd);
|
|
on(display.scroller, "drop", dragEnd);
|
|
return;
|
|
}
|
|
e_preventDefault(e);
|
|
if (type == "single") extendSelection(cm.doc, clipPos(doc, start));
|
|
|
|
var startstart = sel.from, startend = sel.to;
|
|
|
|
function doSelect(cur) {
|
|
if (type == "single") {
|
|
extendSelection(cm.doc, clipPos(doc, start), cur);
|
|
return;
|
|
}
|
|
|
|
startstart = clipPos(doc, startstart);
|
|
startend = clipPos(doc, startend);
|
|
if (type == "double") {
|
|
var word = findWordAt(getLine(doc, cur.line).text, cur);
|
|
if (posLess(cur, startstart)) extendSelection(cm.doc, word.from, startend);
|
|
else extendSelection(cm.doc, startstart, word.to);
|
|
} else if (type == "triple") {
|
|
if (posLess(cur, startstart)) extendSelection(cm.doc, startend, clipPos(doc, Pos(cur.line, 0)));
|
|
else extendSelection(cm.doc, startstart, clipPos(doc, Pos(cur.line + 1, 0)));
|
|
}
|
|
}
|
|
|
|
var editorSize = getRect(display.wrapper);
|
|
// Used to ensure timeout re-tries don't fire when another extend
|
|
// happened in the meantime (clearTimeout isn't reliable -- at
|
|
// least on Chrome, the timeouts still happen even when cleared,
|
|
// if the clear happens after their scheduled firing time).
|
|
var counter = 0;
|
|
|
|
function extend(e) {
|
|
var curCount = ++counter;
|
|
var cur = posFromMouse(cm, e, true);
|
|
if (!cur) return;
|
|
if (!posEq(cur, last)) {
|
|
if (!cm.state.focused) onFocus(cm);
|
|
last = cur;
|
|
doSelect(cur);
|
|
var visible = visibleLines(display, doc);
|
|
if (cur.line >= visible.to || cur.line < visible.from)
|
|
setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150);
|
|
} else {
|
|
var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0;
|
|
if (outside) setTimeout(operation(cm, function() {
|
|
if (counter != curCount) return;
|
|
display.scroller.scrollTop += outside;
|
|
extend(e);
|
|
}), 50);
|
|
}
|
|
}
|
|
|
|
function done(e) {
|
|
counter = Infinity;
|
|
var cur = posFromMouse(cm, e);
|
|
if (cur) doSelect(cur);
|
|
e_preventDefault(e);
|
|
focusInput(cm);
|
|
off(document, "mousemove", move);
|
|
off(document, "mouseup", up);
|
|
}
|
|
|
|
var move = operation(cm, function(e) {
|
|
if (!ie && !e_button(e)) done(e);
|
|
else extend(e);
|
|
});
|
|
var up = operation(cm, done);
|
|
on(document, "mousemove", move);
|
|
on(document, "mouseup", up);
|
|
}
|
|
|
|
function onDrop(e) {
|
|
var cm = this;
|
|
if (eventInWidget(cm.display, e) || (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))))
|
|
return;
|
|
e_preventDefault(e);
|
|
var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files;
|
|
if (!pos || isReadOnly(cm)) return;
|
|
if (files && files.length && window.FileReader && window.File) {
|
|
var n = files.length, text = Array(n), read = 0;
|
|
var loadFile = function(file, i) {
|
|
var reader = new FileReader;
|
|
reader.onload = function() {
|
|
text[i] = reader.result;
|
|
if (++read == n) {
|
|
pos = clipPos(cm.doc, pos);
|
|
replaceRange(cm.doc, text.join(""), pos, "around", "paste");
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
};
|
|
for (var i = 0; i < n; ++i) loadFile(files[i], i);
|
|
} else {
|
|
// Don't do a replace if the drop happened inside of the selected text.
|
|
if (cm.state.draggingText && !(posLess(pos, cm.doc.sel.from) || posLess(cm.doc.sel.to, pos))) {
|
|
cm.state.draggingText(e);
|
|
// Ensure the editor is re-focused
|
|
setTimeout(bind(focusInput, cm), 20);
|
|
return;
|
|
}
|
|
try {
|
|
var text = e.dataTransfer.getData("Text");
|
|
if (text) {
|
|
var curFrom = cm.doc.sel.from, curTo = cm.doc.sel.to;
|
|
setSelection(cm.doc, pos, pos);
|
|
if (cm.state.draggingText) replaceRange(cm.doc, "", curFrom, curTo, "paste");
|
|
cm.replaceSelection(text, null, "paste");
|
|
focusInput(cm);
|
|
onFocus(cm);
|
|
}
|
|
}
|
|
catch(e){}
|
|
}
|
|
}
|
|
|
|
function clickInGutter(cm, e) {
|
|
var display = cm.display;
|
|
try { var mX = e.clientX, mY = e.clientY; }
|
|
catch(e) { return false; }
|
|
|
|
if (mX >= Math.floor(getRect(display.gutters).right)) return false;
|
|
e_preventDefault(e);
|
|
if (!hasHandler(cm, "gutterClick")) return true;
|
|
|
|
var lineBox = getRect(display.lineDiv);
|
|
if (mY > lineBox.bottom) return true;
|
|
mY -= lineBox.top - display.viewOffset;
|
|
|
|
for (var i = 0; i < cm.options.gutters.length; ++i) {
|
|
var g = display.gutters.childNodes[i];
|
|
if (g && getRect(g).right >= mX) {
|
|
var line = lineAtHeight(cm.doc, mY);
|
|
var gutter = cm.options.gutters[i];
|
|
signalLater(cm, "gutterClick", cm, line, gutter, e);
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function onDragStart(cm, e) {
|
|
if (eventInWidget(cm.display, e)) return;
|
|
|
|
var txt = cm.getSelection();
|
|
e.dataTransfer.setData("Text", txt);
|
|
|
|
// Use dummy image instead of default browsers image.
|
|
// Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there.
|
|
if (e.dataTransfer.setDragImage && !safari) {
|
|
var img = elt("img", null, null, "position: fixed; left: 0; top: 0;");
|
|
if (opera) {
|
|
img.width = img.height = 1;
|
|
cm.display.wrapper.appendChild(img);
|
|
// Force a relayout, or Opera won't use our image for some obscure reason
|
|
img._top = img.offsetTop;
|
|
}
|
|
e.dataTransfer.setDragImage(img, 0, 0);
|
|
if (opera) img.parentNode.removeChild(img);
|
|
}
|
|
}
|
|
|
|
function setScrollTop(cm, val) {
|
|
if (Math.abs(cm.doc.scrollTop - val) < 2) return;
|
|
cm.doc.scrollTop = val;
|
|
if (!gecko) updateDisplay(cm, [], val);
|
|
if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val;
|
|
if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val;
|
|
if (gecko) updateDisplay(cm, []);
|
|
}
|
|
function setScrollLeft(cm, val, isScroller) {
|
|
if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return;
|
|
val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth);
|
|
cm.doc.scrollLeft = val;
|
|
alignHorizontally(cm);
|
|
if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val;
|
|
if (cm.display.scrollbarH.scrollLeft != val) cm.display.scrollbarH.scrollLeft = val;
|
|
}
|
|
|
|
// Since the delta values reported on mouse wheel events are
|
|
// unstandardized between browsers and even browser versions, and
|
|
// generally horribly unpredictable, this code starts by measuring
|
|
// the scroll effect that the first few mouse wheel events have,
|
|
// and, from that, detects the way it can convert deltas to pixel
|
|
// offsets afterwards.
|
|
//
|
|
// The reason we want to know the amount a wheel event will scroll
|
|
// is that it gives us a chance to update the display before the
|
|
// actual scrolling happens, reducing flickering.
|
|
|
|
var wheelSamples = 0, wheelPixelsPerUnit = null;
|
|
// Fill in a browser-detected starting value on browsers where we
|
|
// know one. These don't have to be accurate -- the result of them
|
|
// being wrong would just be a slight flicker on the first wheel
|
|
// scroll (if it is large enough).
|
|
if (ie) wheelPixelsPerUnit = -.53;
|
|
else if (gecko) wheelPixelsPerUnit = 15;
|
|
else if (chrome) wheelPixelsPerUnit = -.7;
|
|
else if (safari) wheelPixelsPerUnit = -1/3;
|
|
|
|
function onScrollWheel(cm, e) {
|
|
var dx = e.wheelDeltaX, dy = e.wheelDeltaY;
|
|
if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
|
|
if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail;
|
|
else if (dy == null) dy = e.wheelDelta;
|
|
|
|
// Webkit browsers on OS X abort momentum scrolls when the target
|
|
// of the scroll event is removed from the scrollable element.
|
|
// This hack (see related code in patchDisplay) makes sure the
|
|
// element is kept around.
|
|
if (dy && mac && webkit) {
|
|
for (var cur = e.target; cur != scroll; cur = cur.parentNode) {
|
|
if (cur.lineObj) {
|
|
cm.display.currentWheelTarget = cur;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var display = cm.display, scroll = display.scroller;
|
|
// On some browsers, horizontal scrolling will cause redraws to
|
|
// happen before the gutter has been realigned, causing it to
|
|
// wriggle around in a most unseemly way. When we have an
|
|
// estimated pixels/delta value, we just handle horizontal
|
|
// scrolling entirely here. It'll be slightly off from native, but
|
|
// better than glitching out.
|
|
if (dx && !gecko && !opera && wheelPixelsPerUnit != null) {
|
|
if (dy)
|
|
setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight)));
|
|
setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth)));
|
|
e_preventDefault(e);
|
|
display.wheelStartX = null; // Abort measurement, if in progress
|
|
return;
|
|
}
|
|
|
|
if (dy && wheelPixelsPerUnit != null) {
|
|
var pixels = dy * wheelPixelsPerUnit;
|
|
var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight;
|
|
if (pixels < 0) top = Math.max(0, top + pixels - 50);
|
|
else bot = Math.min(cm.doc.height, bot + pixels + 50);
|
|
updateDisplay(cm, [], {top: top, bottom: bot});
|
|
}
|
|
|
|
if (wheelSamples < 20) {
|
|
if (display.wheelStartX == null) {
|
|
display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop;
|
|
display.wheelDX = dx; display.wheelDY = dy;
|
|
setTimeout(function() {
|
|
if (display.wheelStartX == null) return;
|
|
var movedX = scroll.scrollLeft - display.wheelStartX;
|
|
var movedY = scroll.scrollTop - display.wheelStartY;
|
|
var sample = (movedY && display.wheelDY && movedY / display.wheelDY) ||
|
|
(movedX && display.wheelDX && movedX / display.wheelDX);
|
|
display.wheelStartX = display.wheelStartY = null;
|
|
if (!sample) return;
|
|
wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1);
|
|
++wheelSamples;
|
|
}, 200);
|
|
} else {
|
|
display.wheelDX += dx; display.wheelDY += dy;
|
|
}
|
|
}
|
|
}
|
|
|
|
function doHandleBinding(cm, bound, dropShift) {
|
|
if (typeof bound == "string") {
|
|
bound = commands[bound];
|
|
if (!bound) return false;
|
|
}
|
|
// Ensure previous input has been read, so that the handler sees a
|
|
// consistent view of the document
|
|
if (cm.display.pollingFast && readInput(cm)) cm.display.pollingFast = false;
|
|
var doc = cm.doc, prevShift = doc.sel.shift, done = false;
|
|
try {
|
|
if (isReadOnly(cm)) cm.state.suppressEdits = true;
|
|
if (dropShift) doc.sel.shift = false;
|
|
done = bound(cm) != Pass;
|
|
} finally {
|
|
doc.sel.shift = prevShift;
|
|
cm.state.suppressEdits = false;
|
|
}
|
|
return done;
|
|
}
|
|
|
|
function allKeyMaps(cm) {
|
|
var maps = cm.state.keyMaps.slice(0);
|
|
maps.push(cm.options.keyMap);
|
|
if (cm.options.extraKeys) maps.unshift(cm.options.extraKeys);
|
|
return maps;
|
|
}
|
|
|
|
var maybeTransition;
|
|
function handleKeyBinding(cm, e) {
|
|
// Handle auto keymap transitions
|
|
var startMap = getKeyMap(cm.options.keyMap), next = startMap.auto;
|
|
clearTimeout(maybeTransition);
|
|
if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() {
|
|
if (getKeyMap(cm.options.keyMap) == startMap)
|
|
cm.options.keyMap = (next.call ? next.call(null, cm) : next);
|
|
}, 50);
|
|
|
|
var name = keyName(e, true), handled = false;
|
|
if (!name) return false;
|
|
var keymaps = allKeyMaps(cm);
|
|
|
|
if (e.shiftKey) {
|
|
// First try to resolve full name (including 'Shift-'). Failing
|
|
// that, see if there is a cursor-motion command (starting with
|
|
// 'go') bound to the keyname without 'Shift-'.
|
|
handled = lookupKey("Shift-" + name, keymaps, function(b) {return doHandleBinding(cm, b, true);})
|
|
|| lookupKey(name, keymaps, function(b) {
|
|
if (typeof b == "string" && /^go[A-Z]/.test(b)) return doHandleBinding(cm, b);
|
|
});
|
|
} else {
|
|
handled = lookupKey(name, keymaps, function(b) { return doHandleBinding(cm, b); });
|
|
}
|
|
if (handled == "stop") handled = false;
|
|
|
|
if (handled) {
|
|
e_preventDefault(e);
|
|
restartBlink(cm);
|
|
if (ie_lt9) { e.oldKeyCode = e.keyCode; e.keyCode = 0; }
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
function handleCharBinding(cm, e, ch) {
|
|
var handled = lookupKey("'" + ch + "'", allKeyMaps(cm),
|
|
function(b) { return doHandleBinding(cm, b, true); });
|
|
if (handled) {
|
|
e_preventDefault(e);
|
|
restartBlink(cm);
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
var lastStoppedKey = null;
|
|
function onKeyDown(e) {
|
|
var cm = this;
|
|
if (!cm.state.focused) onFocus(cm);
|
|
if (ie && e.keyCode == 27) { e.returnValue = false; }
|
|
if (cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
|
|
var code = e.keyCode;
|
|
// IE does strange things with escape.
|
|
cm.doc.sel.shift = code == 16 || e.shiftKey;
|
|
// First give onKeyEvent option a chance to handle this.
|
|
var handled = handleKeyBinding(cm, e);
|
|
if (opera) {
|
|
lastStoppedKey = handled ? code : null;
|
|
// Opera has no cut event... we try to at least catch the key combo
|
|
if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey))
|
|
cm.replaceSelection("");
|
|
}
|
|
}
|
|
|
|
function onKeyPress(e) {
|
|
var cm = this;
|
|
if (cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
|
|
var keyCode = e.keyCode, charCode = e.charCode;
|
|
if (opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
|
|
if (((opera && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return;
|
|
var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
|
|
if (this.options.electricChars && this.doc.mode.electricChars &&
|
|
this.options.smartIndent && !isReadOnly(this) &&
|
|
this.doc.mode.electricChars.indexOf(ch) > -1)
|
|
setTimeout(operation(cm, function() {indentLine(cm, cm.doc.sel.to.line, "smart");}), 75);
|
|
if (handleCharBinding(cm, e, ch)) return;
|
|
fastPoll(cm);
|
|
}
|
|
|
|
function onFocus(cm) {
|
|
if (cm.options.readOnly == "nocursor") return;
|
|
if (!cm.state.focused) {
|
|
signal(cm, "focus", cm);
|
|
cm.state.focused = true;
|
|
if (cm.display.wrapper.className.search(/\bCodeMirror-focused\b/) == -1)
|
|
cm.display.wrapper.className += " CodeMirror-focused";
|
|
resetInput(cm, true);
|
|
}
|
|
slowPoll(cm);
|
|
restartBlink(cm);
|
|
}
|
|
function onBlur(cm) {
|
|
if (cm.state.focused) {
|
|
signal(cm, "blur", cm);
|
|
cm.state.focused = false;
|
|
cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-focused", "");
|
|
}
|
|
clearInterval(cm.display.blinker);
|
|
setTimeout(function() {if (!cm.state.focused) cm.doc.sel.shift = false;}, 150);
|
|
}
|
|
|
|
var detectingSelectAll;
|
|
function onContextMenu(cm, e) {
|
|
var display = cm.display, sel = cm.doc.sel;
|
|
if (eventInWidget(display, e)) return;
|
|
|
|
var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop;
|
|
if (!pos || opera) return; // Opera is difficult.
|
|
if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to))
|
|
operation(cm, setSelection)(cm.doc, pos, pos);
|
|
|
|
var oldCSS = display.input.style.cssText;
|
|
display.inputDiv.style.position = "absolute";
|
|
display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) +
|
|
"px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; outline: none;" +
|
|
"border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);";
|
|
focusInput(cm);
|
|
resetInput(cm, true);
|
|
// Adds "Select all" to context menu in FF
|
|
if (posEq(sel.from, sel.to)) display.input.value = display.prevInput = " ";
|
|
|
|
function rehide() {
|
|
display.inputDiv.style.position = "relative";
|
|
display.input.style.cssText = oldCSS;
|
|
if (ie_lt9) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos;
|
|
slowPoll(cm);
|
|
|
|
// Try to detect the user choosing select-all
|
|
if (display.input.selectionStart != null && (!ie || ie_lt9)) {
|
|
clearTimeout(detectingSelectAll);
|
|
var extval = display.input.value = " " + (posEq(sel.from, sel.to) ? "" : display.input.value), i = 0;
|
|
display.prevInput = " ";
|
|
display.input.selectionStart = 1; display.input.selectionEnd = extval.length;
|
|
var poll = function(){
|
|
if (display.prevInput == " " && display.input.selectionStart == 0)
|
|
operation(cm, commands.selectAll)(cm);
|
|
else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500);
|
|
else resetInput(cm);
|
|
};
|
|
detectingSelectAll = setTimeout(poll, 200);
|
|
}
|
|
}
|
|
|
|
if (captureMiddleClick) {
|
|
e_stop(e);
|
|
var mouseup = function() {
|
|
off(window, "mouseup", mouseup);
|
|
setTimeout(rehide, 20);
|
|
};
|
|
on(window, "mouseup", mouseup);
|
|
} else {
|
|
setTimeout(rehide, 50);
|
|
}
|
|
}
|
|
|
|
// UPDATING
|
|
|
|
function changeEnd(change) {
|
|
return Pos(change.from.line + change.text.length - 1,
|
|
lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0));
|
|
}
|
|
|
|
// Make sure a position will be valid after the given change.
|
|
function clipPostChange(doc, change, pos) {
|
|
if (!posLess(change.from, pos)) return clipPos(doc, pos);
|
|
var diff = (change.text.length - 1) - (change.to.line - change.from.line);
|
|
if (pos.line > change.to.line + diff) {
|
|
var preLine = pos.line - diff, lastLine = doc.first + doc.size - 1;
|
|
if (preLine > lastLine) return Pos(lastLine, getLine(doc, lastLine).text.length);
|
|
return clipToLen(pos, getLine(doc, preLine).text.length);
|
|
}
|
|
if (pos.line == change.to.line + diff)
|
|
return clipToLen(pos, lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0) +
|
|
getLine(doc, change.to.line).text.length - change.to.ch);
|
|
var inside = pos.line - change.from.line;
|
|
return clipToLen(pos, change.text[inside].length + (inside ? 0 : change.from.ch));
|
|
}
|
|
|
|
// Hint can be null|"end"|"start"|"around"|{anchor,head}
|
|
function computeSelAfterChange(doc, change, hint) {
|
|
if (hint && typeof hint == "object") // Assumed to be {anchor, head} object
|
|
return {anchor: clipPostChange(doc, change, hint.anchor),
|
|
head: clipPostChange(doc, change, hint.head)};
|
|
|
|
if (hint == "start") return {anchor: change.from, head: change.from};
|
|
|
|
var end = changeEnd(change);
|
|
if (hint == "around") return {anchor: change.from, head: end};
|
|
if (hint == "end") return {anchor: end, head: end};
|
|
|
|
// hint is null, leave the selection alone as much as possible
|
|
var adjustPos = function(pos) {
|
|
if (posLess(pos, change.from)) return pos;
|
|
if (!posLess(change.to, pos)) return end;
|
|
|
|
var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch;
|
|
if (pos.line == change.to.line) ch += end.ch - change.to.ch;
|
|
return Pos(line, ch);
|
|
};
|
|
return {anchor: adjustPos(doc.sel.anchor), head: adjustPos(doc.sel.head)};
|
|
}
|
|
|
|
function filterChange(doc, change) {
|
|
var obj = {
|
|
canceled: false,
|
|
from: change.from,
|
|
to: change.to,
|
|
text: change.text,
|
|
origin: change.origin,
|
|
update: function(from, to, text, origin) {
|
|
if (from) this.from = clipPos(doc, from);
|
|
if (to) this.to = clipPos(doc, to);
|
|
if (text) this.text = text;
|
|
if (origin !== undefined) this.origin = origin;
|
|
},
|
|
cancel: function() { this.canceled = true; }
|
|
};
|
|
signal(doc, "beforeChange", doc, obj);
|
|
if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj);
|
|
|
|
if (obj.canceled) return null;
|
|
return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin};
|
|
}
|
|
|
|
// Replace the range from from to to by the strings in replacement.
|
|
// change is a {from, to, text [, origin]} object
|
|
function makeChange(doc, change, selUpdate, ignoreReadOnly) {
|
|
if (doc.cm) {
|
|
if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, selUpdate, ignoreReadOnly);
|
|
if (doc.cm.state.suppressEdits) return;
|
|
}
|
|
|
|
if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
|
|
change = filterChange(doc, change);
|
|
if (!change) return;
|
|
}
|
|
|
|
// Possibly split or suppress the update based on the presence
|
|
// of read-only spans in its range.
|
|
var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to);
|
|
if (split) {
|
|
for (var i = split.length - 1; i >= 1; --i)
|
|
makeChangeNoReadonly(doc, {from: split[i].from, to: split[i].to, text: [""]});
|
|
if (split.length)
|
|
makeChangeNoReadonly(doc, {from: split[0].from, to: split[0].to, text: change.text}, selUpdate);
|
|
} else {
|
|
makeChangeNoReadonly(doc, change, selUpdate);
|
|
}
|
|
}
|
|
|
|
function makeChangeNoReadonly(doc, change, selUpdate) {
|
|
var selAfter = computeSelAfterChange(doc, change, selUpdate);
|
|
addToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN);
|
|
|
|
makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change));
|
|
var rebased = [];
|
|
|
|
linkedDocs(doc, function(doc, sharedHist) {
|
|
if (!sharedHist && indexOf(rebased, doc.history) == -1) {
|
|
rebaseHist(doc.history, change);
|
|
rebased.push(doc.history);
|
|
}
|
|
makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change));
|
|
});
|
|
}
|
|
|
|
function makeChangeFromHistory(doc, type) {
|
|
var hist = doc.history;
|
|
var event = (type == "undo" ? hist.done : hist.undone).pop();
|
|
if (!event) return;
|
|
hist.dirtyCounter += type == "undo" ? -1 : 1;
|
|
|
|
var anti = {changes: [], anchorBefore: event.anchorAfter, headBefore: event.headAfter,
|
|
anchorAfter: event.anchorBefore, headAfter: event.headBefore};
|
|
(type == "undo" ? hist.undone : hist.done).push(anti);
|
|
|
|
for (var i = event.changes.length - 1; i >= 0; --i) {
|
|
var change = event.changes[i];
|
|
change.origin = type;
|
|
anti.changes.push(historyChangeFromChange(doc, change));
|
|
|
|
var after = i ? computeSelAfterChange(doc, change, null)
|
|
: {anchor: event.anchorBefore, head: event.headBefore};
|
|
makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change));
|
|
var rebased = [];
|
|
|
|
linkedDocs(doc, function(doc, sharedHist) {
|
|
if (!sharedHist && indexOf(rebased, doc.history) == -1) {
|
|
rebaseHist(doc.history, change);
|
|
rebased.push(doc.history);
|
|
}
|
|
makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change));
|
|
});
|
|
}
|
|
}
|
|
|
|
function shiftDoc(doc, distance) {
|
|
function shiftPos(pos) {return Pos(pos.line + distance, pos.ch);}
|
|
doc.first += distance;
|
|
if (doc.cm) regChange(doc.cm, doc.first, doc.first, distance);
|
|
doc.sel.head = shiftPos(doc.sel.head); doc.sel.anchor = shiftPos(doc.sel.anchor);
|
|
doc.sel.from = shiftPos(doc.sel.from); doc.sel.to = shiftPos(doc.sel.to);
|
|
}
|
|
|
|
function makeChangeSingleDoc(doc, change, selAfter, spans) {
|
|
if (doc.cm && !doc.cm.curOp)
|
|
return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans);
|
|
|
|
if (change.to.line < doc.first) {
|
|
shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line));
|
|
return;
|
|
}
|
|
if (change.from.line > doc.lastLine()) return;
|
|
|
|
// Clip the change to the size of this doc
|
|
if (change.from.line < doc.first) {
|
|
var shift = change.text.length - 1 - (doc.first - change.from.line);
|
|
shiftDoc(doc, shift);
|
|
change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
|
|
text: [lst(change.text)], origin: change.origin};
|
|
}
|
|
var last = doc.lastLine();
|
|
if (change.to.line > last) {
|
|
change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
|
|
text: [change.text[0]], origin: change.origin};
|
|
}
|
|
|
|
if (!selAfter) selAfter = computeSelAfterChange(doc, change, null);
|
|
if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans, selAfter);
|
|
else updateDoc(doc, change, spans, selAfter);
|
|
}
|
|
|
|
function makeChangeSingleDocInEditor(cm, change, spans, selAfter) {
|
|
var doc = cm.doc, display = cm.display, from = change.from, to = change.to;
|
|
|
|
var recomputeMaxLength = false, checkWidthStart = from.line;
|
|
if (!cm.options.lineWrapping) {
|
|
checkWidthStart = lineNo(visualLine(doc, getLine(doc, from.line)));
|
|
doc.iter(checkWidthStart, to.line + 1, function(line) {
|
|
if (line == display.maxLine) {
|
|
recomputeMaxLength = true;
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
updateDoc(doc, change, spans, selAfter, estimateHeight(cm));
|
|
|
|
if (!cm.options.lineWrapping) {
|
|
doc.iter(checkWidthStart, from.line + change.text.length, function(line) {
|
|
var len = lineLength(doc, line);
|
|
if (len > display.maxLineLength) {
|
|
display.maxLine = line;
|
|
display.maxLineLength = len;
|
|
display.maxLineChanged = true;
|
|
recomputeMaxLength = false;
|
|
}
|
|
});
|
|
if (recomputeMaxLength) cm.curOp.updateMaxLine = true;
|
|
}
|
|
|
|
// Adjust frontier, schedule worker
|
|
doc.frontier = Math.min(doc.frontier, from.line);
|
|
startWorker(cm, 400);
|
|
|
|
var lendiff = change.text.length - (to.line - from.line) - 1;
|
|
// Remember that these lines changed, for updating the display
|
|
regChange(cm, from.line, to.line + 1, lendiff);
|
|
if (hasHandler(cm, "change")) {
|
|
var changeObj = {from: from, to: to, text: change.text, origin: change.origin};
|
|
if (cm.curOp.textChanged) {
|
|
for (var cur = cm.curOp.textChanged; cur.next; cur = cur.next) {}
|
|
cur.next = changeObj;
|
|
} else cm.curOp.textChanged = changeObj;
|
|
}
|
|
}
|
|
|
|
function replaceRange(doc, code, from, to, origin) {
|
|
if (!to) to = from;
|
|
if (posLess(to, from)) { var tmp = to; to = from; from = tmp; }
|
|
if (typeof code == "string") code = splitLines(code);
|
|
makeChange(doc, {from: from, to: to, text: code, origin: origin}, null);
|
|
}
|
|
|
|
// POSITION OBJECT
|
|
|
|
function Pos(line, ch) {
|
|
if (!(this instanceof Pos)) return new Pos(line, ch);
|
|
this.line = line; this.ch = ch;
|
|
}
|
|
CodeMirror.Pos = Pos;
|
|
|
|
function posEq(a, b) {return a.line == b.line && a.ch == b.ch;}
|
|
function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);}
|
|
function copyPos(x) {return Pos(x.line, x.ch);}
|
|
|
|
// SELECTION
|
|
|
|
function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));}
|
|
function clipPos(doc, pos) {
|
|
if (pos.line < doc.first) return Pos(doc.first, 0);
|
|
var last = doc.first + doc.size - 1;
|
|
if (pos.line > last) return Pos(last, getLine(doc, last).text.length);
|
|
return clipToLen(pos, getLine(doc, pos.line).text.length);
|
|
}
|
|
function clipToLen(pos, linelen) {
|
|
var ch = pos.ch;
|
|
if (ch == null || ch > linelen) return Pos(pos.line, linelen);
|
|
else if (ch < 0) return Pos(pos.line, 0);
|
|
else return pos;
|
|
}
|
|
function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;}
|
|
|
|
// If shift is held, this will move the selection anchor. Otherwise,
|
|
// it'll set the whole selection.
|
|
function extendSelection(doc, pos, other, bias) {
|
|
if (doc.sel.shift || doc.sel.extend) {
|
|
var anchor = doc.sel.anchor;
|
|
if (other) {
|
|
var posBefore = posLess(pos, anchor);
|
|
if (posBefore != posLess(other, anchor)) {
|
|
anchor = pos;
|
|
pos = other;
|
|
} else if (posBefore != posLess(pos, other)) {
|
|
pos = other;
|
|
}
|
|
}
|
|
setSelection(doc, anchor, pos, bias);
|
|
} else {
|
|
setSelection(doc, pos, other || pos, bias);
|
|
}
|
|
if (doc.cm) doc.cm.curOp.userSelChange = true;
|
|
}
|
|
|
|
function filterSelectionChange(doc, anchor, head) {
|
|
var obj = {anchor: anchor, head: head};
|
|
signal(doc, "beforeSelectionChange", doc, obj);
|
|
if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj);
|
|
obj.anchor = clipPos(doc, obj.anchor); obj.head = clipPos(doc, obj.head);
|
|
return obj;
|
|
}
|
|
|
|
// Update the selection. Last two args are only used by
|
|
// updateDoc, since they have to be expressed in the line
|
|
// numbers before the update.
|
|
function setSelection(doc, anchor, head, bias, checkAtomic) {
|
|
if (!checkAtomic && hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) {
|
|
var filtered = filterSelectionChange(doc, anchor, head);
|
|
head = filtered.head;
|
|
anchor = filtered.anchor;
|
|
}
|
|
|
|
var sel = doc.sel;
|
|
sel.goalColumn = null;
|
|
// Skip over atomic spans.
|
|
if (checkAtomic || !posEq(anchor, sel.anchor))
|
|
anchor = skipAtomic(doc, anchor, bias, checkAtomic != "push");
|
|
if (checkAtomic || !posEq(head, sel.head))
|
|
head = skipAtomic(doc, head, bias, checkAtomic != "push");
|
|
|
|
if (posEq(sel.anchor, anchor) && posEq(sel.head, head)) return;
|
|
|
|
sel.anchor = anchor; sel.head = head;
|
|
var inv = posLess(head, anchor);
|
|
sel.from = inv ? head : anchor;
|
|
sel.to = inv ? anchor : head;
|
|
|
|
if (doc.cm)
|
|
doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true;
|
|
|
|
signalLater(doc, "cursorActivity", doc);
|
|
}
|
|
|
|
function reCheckSelection(cm) {
|
|
setSelection(cm.doc, cm.doc.sel.from, cm.doc.sel.to, null, "push");
|
|
}
|
|
|
|
function skipAtomic(doc, pos, bias, mayClear) {
|
|
var flipped = false, curPos = pos;
|
|
var dir = bias || 1;
|
|
doc.cantEdit = false;
|
|
search: for (;;) {
|
|
var line = getLine(doc, curPos.line), toClear;
|
|
if (line.markedSpans) {
|
|
for (var i = 0; i < line.markedSpans.length; ++i) {
|
|
var sp = line.markedSpans[i], m = sp.marker;
|
|
if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) &&
|
|
(sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) {
|
|
if (mayClear && m.clearOnEnter) {
|
|
(toClear || (toClear = [])).push(m);
|
|
continue;
|
|
} else if (!m.atomic) continue;
|
|
var newPos = m.find()[dir < 0 ? "from" : "to"];
|
|
if (posEq(newPos, curPos)) {
|
|
newPos.ch += dir;
|
|
if (newPos.ch < 0) {
|
|
if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1));
|
|
else newPos = null;
|
|
} else if (newPos.ch > line.text.length) {
|
|
if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0);
|
|
else newPos = null;
|
|
}
|
|
if (!newPos) {
|
|
if (flipped) {
|
|
// Driven in a corner -- no valid cursor position found at all
|
|
// -- try again *with* clearing, if we didn't already
|
|
if (!mayClear) return skipAtomic(doc, pos, bias, true);
|
|
// Otherwise, turn off editing until further notice, and return the start of the doc
|
|
doc.cantEdit = true;
|
|
return Pos(doc.first, 0);
|
|
}
|
|
flipped = true; newPos = pos; dir = -dir;
|
|
}
|
|
}
|
|
curPos = newPos;
|
|
continue search;
|
|
}
|
|
}
|
|
if (toClear) for (var i = 0; i < toClear.length; ++i) toClear[i].clear();
|
|
}
|
|
return curPos;
|
|
}
|
|
}
|
|
|
|
// SCROLLING
|
|
|
|
function scrollCursorIntoView(cm) {
|
|
var coords = scrollPosIntoView(cm, cm.doc.sel.head);
|
|
if (!cm.state.focused) return;
|
|
var display = cm.display, box = getRect(display.sizer), doScroll = null;
|
|
if (coords.top + box.top < 0) doScroll = true;
|
|
else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false;
|
|
if (doScroll != null && !phantom) {
|
|
var hidden = display.cursor.style.display == "none";
|
|
if (hidden) {
|
|
display.cursor.style.display = "";
|
|
display.cursor.style.left = coords.left + "px";
|
|
display.cursor.style.top = (coords.top - display.viewOffset) + "px";
|
|
}
|
|
display.cursor.scrollIntoView(doScroll);
|
|
if (hidden) display.cursor.style.display = "none";
|
|
}
|
|
}
|
|
|
|
function scrollPosIntoView(cm, pos) {
|
|
for (;;) {
|
|
var changed = false, coords = cursorCoords(cm, pos);
|
|
var scrollPos = calculateScrollPos(cm, coords.left, coords.top, coords.left, coords.bottom);
|
|
var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft;
|
|
if (scrollPos.scrollTop != null) {
|
|
setScrollTop(cm, scrollPos.scrollTop);
|
|
if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true;
|
|
}
|
|
if (scrollPos.scrollLeft != null) {
|
|
setScrollLeft(cm, scrollPos.scrollLeft);
|
|
if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true;
|
|
}
|
|
if (!changed) return coords;
|
|
}
|
|
}
|
|
|
|
function scrollIntoView(cm, x1, y1, x2, y2) {
|
|
var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2);
|
|
if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop);
|
|
if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft);
|
|
}
|
|
|
|
function calculateScrollPos(cm, x1, y1, x2, y2) {
|
|
var display = cm.display, pt = paddingTop(display);
|
|
y1 += pt; y2 += pt;
|
|
var screen = display.scroller.clientHeight - scrollerCutOff, screentop = display.scroller.scrollTop, result = {};
|
|
var docBottom = cm.doc.height + 2 * pt;
|
|
var atTop = y1 < pt + 10, atBottom = y2 + pt > docBottom - 10;
|
|
if (y1 < screentop) result.scrollTop = atTop ? 0 : Math.max(0, y1);
|
|
else if (y2 > screentop + screen) result.scrollTop = (atBottom ? docBottom : y2) - screen;
|
|
|
|
var screenw = display.scroller.clientWidth - scrollerCutOff, screenleft = display.scroller.scrollLeft;
|
|
x1 += display.gutters.offsetWidth; x2 += display.gutters.offsetWidth;
|
|
var gutterw = display.gutters.offsetWidth;
|
|
var atLeft = x1 < gutterw + 10;
|
|
if (x1 < screenleft + gutterw || atLeft) {
|
|
if (atLeft) x1 = 0;
|
|
result.scrollLeft = Math.max(0, x1 - 10 - gutterw);
|
|
} else if (x2 > screenw + screenleft - 3) {
|
|
result.scrollLeft = x2 + 10 - screenw;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// API UTILITIES
|
|
|
|
function indentLine(cm, n, how, aggressive) {
|
|
var doc = cm.doc;
|
|
if (!how) how = "add";
|
|
if (how == "smart") {
|
|
if (!cm.doc.mode.indent) how = "prev";
|
|
else var state = getStateBefore(cm, n);
|
|
}
|
|
|
|
var tabSize = cm.options.tabSize;
|
|
var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize);
|
|
var curSpaceString = line.text.match(/^\s*/)[0], indentation;
|
|
if (how == "smart") {
|
|
indentation = cm.doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
|
|
if (indentation == Pass) {
|
|
if (!aggressive) return;
|
|
how = "prev";
|
|
}
|
|
}
|
|
if (how == "prev") {
|
|
if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize);
|
|
else indentation = 0;
|
|
} else if (how == "add") {
|
|
indentation = curSpace + cm.options.indentUnit;
|
|
} else if (how == "subtract") {
|
|
indentation = curSpace - cm.options.indentUnit;
|
|
}
|
|
indentation = Math.max(0, indentation);
|
|
|
|
var indentString = "", pos = 0;
|
|
if (cm.options.indentWithTabs)
|
|
for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";}
|
|
if (pos < indentation) indentString += spaceStr(indentation - pos);
|
|
|
|
if (indentString != curSpaceString)
|
|
replaceRange(cm.doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
|
|
line.stateAfter = null;
|
|
}
|
|
|
|
function changeLine(cm, handle, op) {
|
|
var no = handle, line = handle, doc = cm.doc;
|
|
if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle));
|
|
else no = lineNo(handle);
|
|
if (no == null) return null;
|
|
if (op(line, no)) regChange(cm, no, no + 1);
|
|
else return null;
|
|
return line;
|
|
}
|
|
|
|
function findPosH(doc, pos, dir, unit, visually) {
|
|
var line = pos.line, ch = pos.ch;
|
|
var lineObj = getLine(doc, line);
|
|
var possible = true;
|
|
function findNextLine() {
|
|
var l = line + dir;
|
|
if (l < doc.first || l >= doc.first + doc.size) return (possible = false);
|
|
line = l;
|
|
return lineObj = getLine(doc, l);
|
|
}
|
|
function moveOnce(boundToLine) {
|
|
var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true);
|
|
if (next == null) {
|
|
if (!boundToLine && findNextLine()) {
|
|
if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj);
|
|
else ch = dir < 0 ? lineObj.text.length : 0;
|
|
} else return (possible = false);
|
|
} else ch = next;
|
|
return true;
|
|
}
|
|
|
|
if (unit == "char") moveOnce();
|
|
else if (unit == "column") moveOnce(true);
|
|
else if (unit == "word") {
|
|
var sawWord = false;
|
|
for (;;) {
|
|
if (dir < 0) if (!moveOnce()) break;
|
|
if (isWordChar(lineObj.text.charAt(ch))) sawWord = true;
|
|
else if (sawWord) {if (dir < 0) {dir = 1; moveOnce();} break;}
|
|
if (dir > 0) if (!moveOnce()) break;
|
|
}
|
|
}
|
|
var result = skipAtomic(doc, Pos(line, ch), dir, true);
|
|
if (!possible) result.hitSide = true;
|
|
return result;
|
|
}
|
|
|
|
function findPosV(cm, pos, dir, unit) {
|
|
var doc = cm.doc, x = pos.left, y;
|
|
if (unit == "page") {
|
|
var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight);
|
|
y = pos.top + dir * pageSize;
|
|
} else if (unit == "line") {
|
|
y = dir > 0 ? pos.bottom + 3 : pos.top - 3;
|
|
}
|
|
for (;;) {
|
|
var target = coordsChar(cm, x, y);
|
|
if (!target.outside) break;
|
|
if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; }
|
|
y += dir * 5;
|
|
}
|
|
return target;
|
|
}
|
|
|
|
function findWordAt(line, pos) {
|
|
var start = pos.ch, end = pos.ch;
|
|
if (line) {
|
|
if (pos.after === false || end == line.length) --start; else ++end;
|
|
var startChar = line.charAt(start);
|
|
var check = isWordChar(startChar) ? isWordChar :
|
|
/\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} :
|
|
function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
|
|
while (start > 0 && check(line.charAt(start - 1))) --start;
|
|
while (end < line.length && check(line.charAt(end))) ++end;
|
|
}
|
|
return {from: Pos(pos.line, start), to: Pos(pos.line, end)};
|
|
}
|
|
|
|
function selectLine(cm, line) {
|
|
extendSelection(cm.doc, Pos(line, 0), clipPos(cm.doc, Pos(line + 1, 0)));
|
|
}
|
|
|
|
// PROTOTYPE
|
|
|
|
// The publicly visible API. Note that operation(null, f) means
|
|
// 'wrap f in an operation, performed on its `this` parameter'
|
|
|
|
CodeMirror.prototype = {
|
|
focus: function(){window.focus(); focusInput(this); onFocus(this); fastPoll(this);},
|
|
|
|
setOption: function(option, value) {
|
|
var options = this.options, old = options[option];
|
|
if (options[option] == value && option != "mode") return;
|
|
options[option] = value;
|
|
if (optionHandlers.hasOwnProperty(option))
|
|
operation(this, optionHandlers[option])(this, value, old);
|
|
},
|
|
|
|
getOption: function(option) {return this.options[option];},
|
|
getDoc: function() {return this.doc;},
|
|
|
|
addKeyMap: function(map) {
|
|
this.state.keyMaps.push(map);
|
|
},
|
|
removeKeyMap: function(map) {
|
|
var maps = this.state.keyMaps;
|
|
for (var i = 0; i < maps.length; ++i)
|
|
if ((typeof map == "string" ? maps[i].name : maps[i]) == map) {
|
|
maps.splice(i, 1);
|
|
return true;
|
|
}
|
|
},
|
|
|
|
addOverlay: operation(null, function(spec, options) {
|
|
var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec);
|
|
if (mode.startState) throw new Error("Overlays may not be stateful.");
|
|
this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque});
|
|
this.state.modeGen++;
|
|
regChange(this);
|
|
}),
|
|
removeOverlay: operation(null, function(spec) {
|
|
var overlays = this.state.overlays;
|
|
for (var i = 0; i < overlays.length; ++i) {
|
|
if (overlays[i].modeSpec == spec) {
|
|
overlays.splice(i, 1);
|
|
this.state.modeGen++;
|
|
regChange(this);
|
|
return;
|
|
}
|
|
}
|
|
}),
|
|
|
|
indentLine: operation(null, function(n, dir, aggressive) {
|
|
if (typeof dir != "string") {
|
|
if (dir == null) dir = this.options.smartIndent ? "smart" : "prev";
|
|
else dir = dir ? "add" : "subtract";
|
|
}
|
|
if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive);
|
|
}),
|
|
indentSelection: operation(null, function(how) {
|
|
var sel = this.doc.sel;
|
|
if (posEq(sel.from, sel.to)) return indentLine(this, sel.from.line, how);
|
|
var e = sel.to.line - (sel.to.ch ? 0 : 1);
|
|
for (var i = sel.from.line; i <= e; ++i) indentLine(this, i, how);
|
|
}),
|
|
|
|
// Fetch the parser token for a given character. Useful for hacks
|
|
// that want to inspect the mode state (say, for completion).
|
|
getTokenAt: function(pos) {
|
|
var doc = this.doc;
|
|
pos = clipPos(doc, pos);
|
|
var state = getStateBefore(this, pos.line), mode = this.doc.mode;
|
|
var line = getLine(doc, pos.line);
|
|
var stream = new StringStream(line.text, this.options.tabSize);
|
|
while (stream.pos < pos.ch && !stream.eol()) {
|
|
stream.start = stream.pos;
|
|
var style = mode.token(stream, state);
|
|
}
|
|
return {start: stream.start,
|
|
end: stream.pos,
|
|
string: stream.current(),
|
|
className: style || null, // Deprecated, use 'type' instead
|
|
type: style || null,
|
|
state: state};
|
|
},
|
|
|
|
getStateAfter: function(line) {
|
|
var doc = this.doc;
|
|
line = clipLine(doc, line == null ? doc.first + doc.size - 1: line);
|
|
return getStateBefore(this, line + 1);
|
|
},
|
|
|
|
cursorCoords: function(start, mode) {
|
|
var pos, sel = this.doc.sel;
|
|
if (start == null) pos = sel.head;
|
|
else if (typeof start == "object") pos = clipPos(this.doc, start);
|
|
else pos = start ? sel.from : sel.to;
|
|
return cursorCoords(this, pos, mode || "page");
|
|
},
|
|
|
|
charCoords: function(pos, mode) {
|
|
return charCoords(this, clipPos(this.doc, pos), mode || "page");
|
|
},
|
|
|
|
coordsChar: function(coords) {
|
|
var off = getRect(this.display.lineSpace);
|
|
var scrollY = window.pageYOffset || (document.documentElement || document.body).scrollTop;
|
|
var scrollX = window.pageXOffset || (document.documentElement || document.body).scrollLeft;
|
|
return coordsChar(this, coords.left - off.left - scrollX, coords.top - off.top - scrollY);
|
|
},
|
|
|
|
defaultTextHeight: function() { return textHeight(this.display); },
|
|
|
|
setGutterMarker: operation(null, function(line, gutterID, value) {
|
|
return changeLine(this, line, function(line) {
|
|
var markers = line.gutterMarkers || (line.gutterMarkers = {});
|
|
markers[gutterID] = value;
|
|
if (!value && isEmpty(markers)) line.gutterMarkers = null;
|
|
return true;
|
|
});
|
|
}),
|
|
|
|
clearGutter: operation(null, function(gutterID) {
|
|
var cm = this, doc = cm.doc, i = doc.first;
|
|
doc.iter(function(line) {
|
|
if (line.gutterMarkers && line.gutterMarkers[gutterID]) {
|
|
line.gutterMarkers[gutterID] = null;
|
|
regChange(cm, i, i + 1);
|
|
if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null;
|
|
}
|
|
++i;
|
|
});
|
|
}),
|
|
|
|
addLineClass: operation(null, function(handle, where, cls) {
|
|
return changeLine(this, handle, function(line) {
|
|
var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass";
|
|
if (!line[prop]) line[prop] = cls;
|
|
else if (new RegExp("\\b" + cls + "\\b").test(line[prop])) return false;
|
|
else line[prop] += " " + cls;
|
|
return true;
|
|
});
|
|
}),
|
|
|
|
removeLineClass: operation(null, function(handle, where, cls) {
|
|
return changeLine(this, handle, function(line) {
|
|
var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass";
|
|
var cur = line[prop];
|
|
if (!cur) return false;
|
|
else if (cls == null) line[prop] = null;
|
|
else {
|
|
var upd = cur.replace(new RegExp("^" + cls + "\\b\\s*|\\s*\\b" + cls + "\\b"), "");
|
|
if (upd == cur) return false;
|
|
line[prop] = upd || null;
|
|
}
|
|
return true;
|
|
});
|
|
}),
|
|
|
|
addLineWidget: operation(null, function(handle, node, options) {
|
|
return addLineWidget(this, handle, node, options);
|
|
}),
|
|
|
|
removeLineWidget: function(widget) { widget.clear(); },
|
|
|
|
lineInfo: function(line) {
|
|
if (typeof line == "number") {
|
|
if (!isLine(this.doc, line)) return null;
|
|
var n = line;
|
|
line = getLine(this.doc, line);
|
|
if (!line) return null;
|
|
} else {
|
|
var n = lineNo(line);
|
|
if (n == null) return null;
|
|
}
|
|
return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers,
|
|
textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass,
|
|
widgets: line.widgets};
|
|
},
|
|
|
|
getViewport: function() { return {from: this.display.showingFrom, to: this.display.showingTo};},
|
|
|
|
addWidget: function(pos, node, scroll, vert, horiz) {
|
|
var display = this.display;
|
|
pos = cursorCoords(this, clipPos(this.doc, pos));
|
|
var top = pos.bottom, left = pos.left;
|
|
node.style.position = "absolute";
|
|
display.sizer.appendChild(node);
|
|
if (vert == "over") {
|
|
top = pos.top;
|
|
} else if (vert == "above" || vert == "near") {
|
|
var vspace = Math.max(display.wrapper.clientHeight, this.doc.height),
|
|
hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth);
|
|
// Default to positioning above (if specified and possible); otherwise default to positioning below
|
|
if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight)
|
|
top = pos.top - node.offsetHeight;
|
|
else if (pos.bottom + node.offsetHeight <= vspace)
|
|
top = pos.bottom;
|
|
if (left + node.offsetWidth > hspace)
|
|
left = hspace - node.offsetWidth;
|
|
}
|
|
node.style.top = (top + paddingTop(display)) + "px";
|
|
node.style.left = node.style.right = "";
|
|
if (horiz == "right") {
|
|
left = display.sizer.clientWidth - node.offsetWidth;
|
|
node.style.right = "0px";
|
|
} else {
|
|
if (horiz == "left") left = 0;
|
|
else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2;
|
|
node.style.left = left + "px";
|
|
}
|
|
if (scroll)
|
|
scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight);
|
|
},
|
|
|
|
triggerOnKeyDown: operation(null, onKeyDown),
|
|
|
|
execCommand: function(cmd) {return commands[cmd](this);},
|
|
|
|
findPosH: function(from, amount, unit, visually) {
|
|
var dir = 1;
|
|
if (amount < 0) { dir = -1; amount = -amount; }
|
|
for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
|
|
cur = findPosH(this.doc, cur, dir, unit, visually);
|
|
if (cur.hitSide) break;
|
|
}
|
|
return cur;
|
|
},
|
|
|
|
moveH: operation(null, function(dir, unit) {
|
|
var sel = this.doc.sel, pos;
|
|
if (sel.shift || sel.extend || posEq(sel.from, sel.to))
|
|
pos = findPosH(this.doc, sel.head, dir, unit, this.options.rtlMoveVisually);
|
|
else
|
|
pos = dir < 0 ? sel.from : sel.to;
|
|
extendSelection(this.doc, pos, pos, dir);
|
|
}),
|
|
|
|
deleteH: operation(null, function(dir, unit) {
|
|
var sel = this.doc.sel;
|
|
if (!posEq(sel.from, sel.to)) replaceRange(this.doc, "", sel.from, sel.to, "+delete");
|
|
else replaceRange(this.doc, "", sel.from, findPosH(this.doc, sel.head, dir, unit, false), "+delete");
|
|
this.curOp.userSelChange = true;
|
|
}),
|
|
|
|
findPosV: function(from, amount, unit, goalColumn) {
|
|
var dir = 1, x = goalColumn;
|
|
if (amount < 0) { dir = -1; amount = -amount; }
|
|
for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
|
|
var coords = cursorCoords(this, cur, "div");
|
|
if (x == null) x = coords.left;
|
|
else coords.left = x;
|
|
cur = findPosV(this, coords, dir, unit);
|
|
if (cur.hitSide) break;
|
|
}
|
|
return cur;
|
|
},
|
|
|
|
moveV: operation(null, function(dir, unit) {
|
|
var sel = this.doc.sel;
|
|
var pos = cursorCoords(this, sel.head, "div");
|
|
if (sel.goalColumn != null) pos.left = sel.goalColumn;
|
|
var target = findPosV(this, pos, dir, unit);
|
|
|
|
if (unit == "page")
|
|
this.display.scrollbarV.scrollTop += charCoords(this, target, "div").top - pos.top;
|
|
extendSelection(this.doc, target, target, dir);
|
|
sel.goalColumn = pos.left;
|
|
}),
|
|
|
|
toggleOverwrite: function() {
|
|
if (this.state.overwrite = !this.state.overwrite)
|
|
this.display.cursor.className += " CodeMirror-overwrite";
|
|
else
|
|
this.display.cursor.className = this.display.cursor.className.replace(" CodeMirror-overwrite", "");
|
|
},
|
|
|
|
scrollTo: operation(null, function(x, y) {
|
|
this.curOp.updateScrollPos = {scrollLeft: x, scrollTop: y};
|
|
}),
|
|
getScrollInfo: function() {
|
|
var scroller = this.display.scroller, co = scrollerCutOff;
|
|
return {left: scroller.scrollLeft, top: scroller.scrollTop,
|
|
height: scroller.scrollHeight - co, width: scroller.scrollWidth - co,
|
|
clientHeight: scroller.clientHeight - co, clientWidth: scroller.clientWidth - co};
|
|
},
|
|
|
|
scrollIntoView: function(pos) {
|
|
if (typeof pos == "number") pos = Pos(pos, 0);
|
|
if (!pos || pos.line != null) {
|
|
pos = pos ? clipPos(this.doc, pos) : this.doc.sel.head;
|
|
scrollPosIntoView(this, pos);
|
|
} else {
|
|
scrollIntoView(this, pos.left, pos.top, pos.right, pos.bottom);
|
|
}
|
|
},
|
|
|
|
setSize: function(width, height) {
|
|
function interpret(val) {
|
|
return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val;
|
|
}
|
|
if (width != null) this.display.wrapper.style.width = interpret(width);
|
|
if (height != null) this.display.wrapper.style.height = interpret(height);
|
|
this.refresh();
|
|
},
|
|
|
|
on: function(type, f) {on(this, type, f);},
|
|
off: function(type, f) {off(this, type, f);},
|
|
|
|
operation: function(f){return runInOp(this, f);},
|
|
|
|
refresh: operation(null, function() {
|
|
clearCaches(this);
|
|
this.curOp.updateScrollPos = {scrollTop: this.doc.scrollTop, scrollLeft: this.doc.scrollLeft};
|
|
regChange(this);
|
|
}),
|
|
|
|
swapDoc: operation(null, function(doc) {
|
|
var old = this.doc;
|
|
old.cm = null;
|
|
attachDoc(this, doc);
|
|
clearCaches(this);
|
|
this.curOp.updateScrollPos = {scrollTop: doc.scrollTop, scrollLeft: doc.scrollLeft};
|
|
return old;
|
|
}),
|
|
|
|
getInputField: function(){return this.display.input;},
|
|
getWrapperElement: function(){return this.display.wrapper;},
|
|
getScrollerElement: function(){return this.display.scroller;},
|
|
getGutterElement: function(){return this.display.gutters;}
|
|
};
|
|
|
|
// OPTION DEFAULTS
|
|
|
|
var optionHandlers = CodeMirror.optionHandlers = {};
|
|
|
|
// The default configuration options.
|
|
var defaults = CodeMirror.defaults = {};
|
|
|
|
function option(name, deflt, handle, notOnInit) {
|
|
CodeMirror.defaults[name] = deflt;
|
|
if (handle) optionHandlers[name] =
|
|
notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle;
|
|
}
|
|
|
|
var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}};
|
|
|
|
// These two are, on init, called from the constructor because they
|
|
// have to be initialized before the editor can start at all.
|
|
option("value", "", function(cm, val) {
|
|
cm.setValue(val);
|
|
}, true);
|
|
option("mode", null, function(cm, val) {
|
|
cm.doc.modeOption = val;
|
|
loadMode(cm);
|
|
}, true);
|
|
|
|
option("indentUnit", 2, loadMode, true);
|
|
option("indentWithTabs", false);
|
|
option("smartIndent", true);
|
|
option("tabSize", 4, function(cm) {
|
|
loadMode(cm);
|
|
clearCaches(cm);
|
|
regChange(cm);
|
|
}, true);
|
|
option("electricChars", true);
|
|
option("rtlMoveVisually", !windows);
|
|
|
|
option("theme", "default", function(cm) {
|
|
themeChanged(cm);
|
|
guttersChanged(cm);
|
|
}, true);
|
|
option("keyMap", "default", keyMapChanged);
|
|
option("extraKeys", null);
|
|
|
|
option("onKeyEvent", null);
|
|
option("onDragEvent", null);
|
|
|
|
option("lineWrapping", false, wrappingChanged, true);
|
|
option("gutters", [], function(cm) {
|
|
setGuttersForLineNumbers(cm.options);
|
|
guttersChanged(cm);
|
|
}, true);
|
|
option("fixedGutter", true, function(cm, val) {
|
|
cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0";
|
|
cm.refresh();
|
|
}, true);
|
|
option("lineNumbers", false, function(cm) {
|
|
setGuttersForLineNumbers(cm.options);
|
|
guttersChanged(cm);
|
|
}, true);
|
|
option("firstLineNumber", 1, guttersChanged, true);
|
|
option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true);
|
|
option("showCursorWhenSelecting", false, updateSelection, true);
|
|
|
|
option("readOnly", false, function(cm, val) {
|
|
if (val == "nocursor") {onBlur(cm); cm.display.input.blur();}
|
|
else if (!val) resetInput(cm, true);
|
|
});
|
|
option("dragDrop", true);
|
|
|
|
option("cursorBlinkRate", 530);
|
|
option("cursorHeight", 1);
|
|
option("workTime", 100);
|
|
option("workDelay", 100);
|
|
option("flattenSpans", true);
|
|
option("pollInterval", 100);
|
|
option("undoDepth", 40, function(cm, val){cm.doc.history.undoDepth = val;});
|
|
option("viewportMargin", 10, function(cm){cm.refresh();}, true);
|
|
|
|
option("tabindex", null, function(cm, val) {
|
|
cm.display.input.tabIndex = val || "";
|
|
});
|
|
option("autofocus", null);
|
|
|
|
// MODE DEFINITION AND QUERYING
|
|
|
|
// Known modes, by name and by MIME
|
|
var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {};
|
|
|
|
CodeMirror.defineMode = function(name, mode) {
|
|
if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name;
|
|
if (arguments.length > 2) {
|
|
mode.dependencies = [];
|
|
for (var i = 2; i < arguments.length; ++i) mode.dependencies.push(arguments[i]);
|
|
}
|
|
modes[name] = mode;
|
|
};
|
|
|
|
CodeMirror.defineMIME = function(mime, spec) {
|
|
mimeModes[mime] = spec;
|
|
};
|
|
|
|
CodeMirror.resolveMode = function(spec) {
|
|
if (typeof spec == "string" && mimeModes.hasOwnProperty(spec))
|
|
spec = mimeModes[spec];
|
|
else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec))
|
|
return CodeMirror.resolveMode("application/xml");
|
|
if (typeof spec == "string") return {name: spec};
|
|
else return spec || {name: "null"};
|
|
};
|
|
|
|
CodeMirror.getMode = function(options, spec) {
|
|
spec = CodeMirror.resolveMode(spec);
|
|
var mfactory = modes[spec.name];
|
|
if (!mfactory) return CodeMirror.getMode(options, "text/plain");
|
|
var modeObj = mfactory(options, spec);
|
|
if (modeExtensions.hasOwnProperty(spec.name)) {
|
|
var exts = modeExtensions[spec.name];
|
|
for (var prop in exts) {
|
|
if (!exts.hasOwnProperty(prop)) continue;
|
|
if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop];
|
|
modeObj[prop] = exts[prop];
|
|
}
|
|
}
|
|
modeObj.name = spec.name;
|
|
return modeObj;
|
|
};
|
|
|
|
CodeMirror.defineMode("null", function() {
|
|
return {token: function(stream) {stream.skipToEnd();}};
|
|
});
|
|
CodeMirror.defineMIME("text/plain", "null");
|
|
|
|
var modeExtensions = CodeMirror.modeExtensions = {};
|
|
CodeMirror.extendMode = function(mode, properties) {
|
|
var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {});
|
|
copyObj(properties, exts);
|
|
};
|
|
|
|
// EXTENSIONS
|
|
|
|
CodeMirror.defineExtension = function(name, func) {
|
|
CodeMirror.prototype[name] = func;
|
|
};
|
|
|
|
CodeMirror.defineOption = option;
|
|
|
|
var initHooks = [];
|
|
CodeMirror.defineInitHook = function(f) {initHooks.push(f);};
|
|
|
|
// MODE STATE HANDLING
|
|
|
|
// Utility functions for working with state. Exported because modes
|
|
// sometimes need to do this.
|
|
function copyState(mode, state) {
|
|
if (state === true) return state;
|
|
if (mode.copyState) return mode.copyState(state);
|
|
var nstate = {};
|
|
for (var n in state) {
|
|
var val = state[n];
|
|
if (val instanceof Array) val = val.concat([]);
|
|
nstate[n] = val;
|
|
}
|
|
return nstate;
|
|
}
|
|
CodeMirror.copyState = copyState;
|
|
|
|
function startState(mode, a1, a2) {
|
|
return mode.startState ? mode.startState(a1, a2) : true;
|
|
}
|
|
CodeMirror.startState = startState;
|
|
|
|
CodeMirror.innerMode = function(mode, state) {
|
|
while (mode.innerMode) {
|
|
var info = mode.innerMode(state);
|
|
state = info.state;
|
|
mode = info.mode;
|
|
}
|
|
return info || {mode: mode, state: state};
|
|
};
|
|
|
|
// STANDARD COMMANDS
|
|
|
|
var commands = CodeMirror.commands = {
|
|
selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()));},
|
|
killLine: function(cm) {
|
|
var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
|
|
if (!sel && cm.getLine(from.line).length == from.ch)
|
|
cm.replaceRange("", from, Pos(from.line + 1, 0), "+delete");
|
|
else cm.replaceRange("", from, sel ? to : Pos(from.line), "+delete");
|
|
},
|
|
deleteLine: function(cm) {
|
|
var l = cm.getCursor().line;
|
|
cm.replaceRange("", Pos(l, 0), Pos(l), "+delete");
|
|
},
|
|
undo: function(cm) {cm.undo();},
|
|
redo: function(cm) {cm.redo();},
|
|
goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));},
|
|
goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));},
|
|
goLineStart: function(cm) {
|
|
cm.extendSelection(lineStart(cm, cm.getCursor().line));
|
|
},
|
|
goLineStartSmart: function(cm) {
|
|
var cur = cm.getCursor(), start = lineStart(cm, cur.line);
|
|
var line = cm.getLineHandle(start.line);
|
|
var order = getOrder(line);
|
|
if (!order || order[0].level == 0) {
|
|
var firstNonWS = Math.max(0, line.text.search(/\S/));
|
|
var inWS = cur.line == start.line && cur.ch <= firstNonWS && cur.ch;
|
|
cm.extendSelection(Pos(start.line, inWS ? 0 : firstNonWS));
|
|
} else cm.extendSelection(start);
|
|
},
|
|
goLineEnd: function(cm) {
|
|
cm.extendSelection(lineEnd(cm, cm.getCursor().line));
|
|
},
|
|
goLineUp: function(cm) {cm.moveV(-1, "line");},
|
|
goLineDown: function(cm) {cm.moveV(1, "line");},
|
|
goPageUp: function(cm) {cm.moveV(-1, "page");},
|
|
goPageDown: function(cm) {cm.moveV(1, "page");},
|
|
goCharLeft: function(cm) {cm.moveH(-1, "char");},
|
|
goCharRight: function(cm) {cm.moveH(1, "char");},
|
|
goColumnLeft: function(cm) {cm.moveH(-1, "column");},
|
|
goColumnRight: function(cm) {cm.moveH(1, "column");},
|
|
goWordLeft: function(cm) {cm.moveH(-1, "word");},
|
|
goWordRight: function(cm) {cm.moveH(1, "word");},
|
|
delCharBefore: function(cm) {cm.deleteH(-1, "char");},
|
|
delCharAfter: function(cm) {cm.deleteH(1, "char");},
|
|
delWordBefore: function(cm) {cm.deleteH(-1, "word");},
|
|
delWordAfter: function(cm) {cm.deleteH(1, "word");},
|
|
indentAuto: function(cm) {cm.indentSelection("smart");},
|
|
indentMore: function(cm) {cm.indentSelection("add");},
|
|
indentLess: function(cm) {cm.indentSelection("subtract");},
|
|
insertTab: function(cm) {cm.replaceSelection("\t", "end", "+input");},
|
|
defaultTab: function(cm) {
|
|
if (cm.somethingSelected()) cm.indentSelection("add");
|
|
else cm.replaceSelection("\t", "end", "+input");
|
|
},
|
|
transposeChars: function(cm) {
|
|
var cur = cm.getCursor(), line = cm.getLine(cur.line);
|
|
if (cur.ch > 0 && cur.ch < line.length - 1)
|
|
cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1),
|
|
Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1));
|
|
},
|
|
newlineAndIndent: function(cm) {
|
|
operation(cm, function() {
|
|
cm.replaceSelection("\n", "end", "+input");
|
|
cm.indentLine(cm.getCursor().line, null, true);
|
|
})();
|
|
},
|
|
toggleOverwrite: function(cm) {cm.toggleOverwrite();}
|
|
};
|
|
|
|
// STANDARD KEYMAPS
|
|
|
|
var keyMap = CodeMirror.keyMap = {};
|
|
keyMap.basic = {
|
|
"Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown",
|
|
"End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown",
|
|
"Delete": "delCharAfter", "Backspace": "delCharBefore", "Tab": "defaultTab", "Shift-Tab": "indentAuto",
|
|
"Enter": "newlineAndIndent", "Insert": "toggleOverwrite"
|
|
};
|
|
// Note that the save and find-related commands aren't defined by
|
|
// default. Unknown commands are simply ignored.
|
|
keyMap.pcDefault = {
|
|
"Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo",
|
|
"Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd",
|
|
"Ctrl-Left": "goWordLeft", "Ctrl-Right": "goWordRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd",
|
|
"Ctrl-Backspace": "delWordBefore", "Ctrl-Delete": "delWordAfter", "Ctrl-S": "save", "Ctrl-F": "find",
|
|
"Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
|
|
"Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
|
|
fallthrough: "basic"
|
|
};
|
|
keyMap.macDefault = {
|
|
"Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
|
|
"Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goWordLeft",
|
|
"Alt-Right": "goWordRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delWordBefore",
|
|
"Ctrl-Alt-Backspace": "delWordAfter", "Alt-Delete": "delWordAfter", "Cmd-S": "save", "Cmd-F": "find",
|
|
"Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
|
|
"Cmd-[": "indentLess", "Cmd-]": "indentMore",
|
|
fallthrough: ["basic", "emacsy"]
|
|
};
|
|
keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault;
|
|
keyMap.emacsy = {
|
|
"Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
|
|
"Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
|
|
"Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore",
|
|
"Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars"
|
|
};
|
|
|
|
// KEYMAP DISPATCH
|
|
|
|
function getKeyMap(val) {
|
|
if (typeof val == "string") return keyMap[val];
|
|
else return val;
|
|
}
|
|
|
|
function lookupKey(name, maps, handle) {
|
|
function lookup(map) {
|
|
map = getKeyMap(map);
|
|
var found = map[name];
|
|
if (found === false) return "stop";
|
|
if (found != null && handle(found)) return true;
|
|
if (map.nofallthrough) return "stop";
|
|
|
|
var fallthrough = map.fallthrough;
|
|
if (fallthrough == null) return false;
|
|
if (Object.prototype.toString.call(fallthrough) != "[object Array]")
|
|
return lookup(fallthrough);
|
|
for (var i = 0, e = fallthrough.length; i < e; ++i) {
|
|
var done = lookup(fallthrough[i]);
|
|
if (done) return done;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
for (var i = 0; i < maps.length; ++i) {
|
|
var done = lookup(maps[i]);
|
|
if (done) return done;
|
|
}
|
|
}
|
|
function isModifierKey(event) {
|
|
var name = keyNames[event.keyCode];
|
|
return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
|
|
}
|
|
function keyName(event, noShift) {
|
|
var name = keyNames[event.keyCode];
|
|
if (name == null || event.altGraphKey) return false;
|
|
if (event.altKey) name = "Alt-" + name;
|
|
if (flipCtrlCmd ? event.metaKey : event.ctrlKey) name = "Ctrl-" + name;
|
|
if (flipCtrlCmd ? event.ctrlKey : event.metaKey) name = "Cmd-" + name;
|
|
if (!noShift && event.shiftKey) name = "Shift-" + name;
|
|
return name;
|
|
}
|
|
CodeMirror.lookupKey = lookupKey;
|
|
CodeMirror.isModifierKey = isModifierKey;
|
|
CodeMirror.keyName = keyName;
|
|
|
|
// FROMTEXTAREA
|
|
|
|
CodeMirror.fromTextArea = function(textarea, options) {
|
|
if (!options) options = {};
|
|
options.value = textarea.value;
|
|
if (!options.tabindex && textarea.tabindex)
|
|
options.tabindex = textarea.tabindex;
|
|
// Set autofocus to true if this textarea is focused, or if it has
|
|
// autofocus and no other element is focused.
|
|
if (options.autofocus == null) {
|
|
var hasFocus = document.body;
|
|
// doc.activeElement occasionally throws on IE
|
|
try { hasFocus = document.activeElement; } catch(e) {}
|
|
options.autofocus = hasFocus == textarea ||
|
|
textarea.getAttribute("autofocus") != null && hasFocus == document.body;
|
|
}
|
|
|
|
function save() {textarea.value = cm.getValue();}
|
|
if (textarea.form) {
|
|
// Deplorable hack to make the submit method do the right thing.
|
|
on(textarea.form, "submit", save);
|
|
var form = textarea.form, realSubmit = form.submit;
|
|
try {
|
|
var wrappedSubmit = form.submit = function() {
|
|
save();
|
|
form.submit = realSubmit;
|
|
form.submit();
|
|
form.submit = wrappedSubmit;
|
|
};
|
|
} catch(e) {}
|
|
}
|
|
|
|
textarea.style.display = "none";
|
|
var cm = CodeMirror(function(node) {
|
|
textarea.parentNode.insertBefore(node, textarea.nextSibling);
|
|
}, options);
|
|
cm.save = save;
|
|
cm.getTextArea = function() { return textarea; };
|
|
cm.toTextArea = function() {
|
|
save();
|
|
textarea.parentNode.removeChild(cm.getWrapperElement());
|
|
textarea.style.display = "";
|
|
if (textarea.form) {
|
|
off(textarea.form, "submit", save);
|
|
if (typeof textarea.form.submit == "function")
|
|
textarea.form.submit = realSubmit;
|
|
}
|
|
};
|
|
return cm;
|
|
};
|
|
|
|
// STRING STREAM
|
|
|
|
// Fed to the mode parsers, provides helper functions to make
|
|
// parsers more succinct.
|
|
|
|
// The character stream used by a mode's parser.
|
|
function StringStream(string, tabSize) {
|
|
this.pos = this.start = 0;
|
|
this.string = string;
|
|
this.tabSize = tabSize || 8;
|
|
}
|
|
|
|
StringStream.prototype = {
|
|
eol: function() {return this.pos >= this.string.length;},
|
|
sol: function() {return this.pos == 0;},
|
|
peek: function() {return this.string.charAt(this.pos) || undefined;},
|
|
next: function() {
|
|
if (this.pos < this.string.length)
|
|
return this.string.charAt(this.pos++);
|
|
},
|
|
eat: function(match) {
|
|
var ch = this.string.charAt(this.pos);
|
|
if (typeof match == "string") var ok = ch == match;
|
|
else var ok = ch && (match.test ? match.test(ch) : match(ch));
|
|
if (ok) {++this.pos; return ch;}
|
|
},
|
|
eatWhile: function(match) {
|
|
var start = this.pos;
|
|
while (this.eat(match)){}
|
|
return this.pos > start;
|
|
},
|
|
eatSpace: function() {
|
|
var start = this.pos;
|
|
while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
|
|
return this.pos > start;
|
|
},
|
|
skipToEnd: function() {this.pos = this.string.length;},
|
|
skipTo: function(ch) {
|
|
var found = this.string.indexOf(ch, this.pos);
|
|
if (found > -1) {this.pos = found; return true;}
|
|
},
|
|
backUp: function(n) {this.pos -= n;},
|
|
column: function() {return countColumn(this.string, this.start, this.tabSize);},
|
|
indentation: function() {return countColumn(this.string, null, this.tabSize);},
|
|
match: function(pattern, consume, caseInsensitive) {
|
|
if (typeof pattern == "string") {
|
|
var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
|
|
if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) {
|
|
if (consume !== false) this.pos += pattern.length;
|
|
return true;
|
|
}
|
|
} else {
|
|
var match = this.string.slice(this.pos).match(pattern);
|
|
if (match && match.index > 0) return null;
|
|
if (match && consume !== false) this.pos += match[0].length;
|
|
return match;
|
|
}
|
|
},
|
|
current: function(){return this.string.slice(this.start, this.pos);}
|
|
};
|
|
CodeMirror.StringStream = StringStream;
|
|
|
|
// TEXTMARKERS
|
|
|
|
function TextMarker(doc, type) {
|
|
this.lines = [];
|
|
this.type = type;
|
|
this.doc = doc;
|
|
}
|
|
CodeMirror.TextMarker = TextMarker;
|
|
|
|
TextMarker.prototype.clear = function() {
|
|
if (this.explicitlyCleared) return;
|
|
var cm = this.doc.cm, withOp = cm && !cm.curOp;
|
|
if (withOp) startOperation(cm);
|
|
var min = null, max = null;
|
|
for (var i = 0; i < this.lines.length; ++i) {
|
|
var line = this.lines[i];
|
|
var span = getMarkedSpanFor(line.markedSpans, this);
|
|
if (span.to != null) max = lineNo(line);
|
|
line.markedSpans = removeMarkedSpan(line.markedSpans, span);
|
|
if (span.from != null)
|
|
min = lineNo(line);
|
|
else if (this.collapsed && !lineIsHidden(this.doc, line) && cm)
|
|
updateLineHeight(line, textHeight(cm.display));
|
|
}
|
|
if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) {
|
|
var visual = visualLine(cm.doc, this.lines[i]), len = lineLength(cm.doc, visual);
|
|
if (len > cm.display.maxLineLength) {
|
|
cm.display.maxLine = visual;
|
|
cm.display.maxLineLength = len;
|
|
cm.display.maxLineChanged = true;
|
|
}
|
|
}
|
|
|
|
if (min != null && cm) regChange(cm, min, max + 1);
|
|
this.lines.length = 0;
|
|
this.explicitlyCleared = true;
|
|
if (this.collapsed && this.doc.cantEdit) {
|
|
this.doc.cantEdit = false;
|
|
if (cm) reCheckSelection(cm);
|
|
}
|
|
if (withOp) endOperation(cm);
|
|
signalLater(this, "clear");
|
|
};
|
|
|
|
TextMarker.prototype.find = function() {
|
|
var from, to;
|
|
for (var i = 0; i < this.lines.length; ++i) {
|
|
var line = this.lines[i];
|
|
var span = getMarkedSpanFor(line.markedSpans, this);
|
|
if (span.from != null || span.to != null) {
|
|
var found = lineNo(line);
|
|
if (span.from != null) from = Pos(found, span.from);
|
|
if (span.to != null) to = Pos(found, span.to);
|
|
}
|
|
}
|
|
if (this.type == "bookmark") return from;
|
|
return from && {from: from, to: to};
|
|
};
|
|
|
|
TextMarker.prototype.getOptions = function(copyWidget) {
|
|
var repl = this.replacedWith;
|
|
return {className: this.className,
|
|
inclusiveLeft: this.inclusiveLeft, inclusiveRight: this.inclusiveRight,
|
|
atomic: this.atomic,
|
|
collapsed: this.collapsed,
|
|
clearOnEnter: this.clearOnEnter,
|
|
replacedWith: copyWidget ? repl && repl.cloneNode(true) : repl,
|
|
readOnly: this.readOnly,
|
|
startStyle: this.startStyle, endStyle: this.endStyle};
|
|
};
|
|
|
|
TextMarker.prototype.attachLine = function(line) {
|
|
if (!this.lines.length && this.doc.cm) {
|
|
var op = this.doc.cm.curOp;
|
|
if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1)
|
|
(op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this);
|
|
}
|
|
this.lines.push(line);
|
|
};
|
|
TextMarker.prototype.detachLine = function(line) {
|
|
this.lines.splice(indexOf(this.lines, line), 1);
|
|
if (!this.lines.length && this.doc.cm) {
|
|
var op = this.doc.cm.curOp;
|
|
(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this);
|
|
}
|
|
};
|
|
|
|
function markText(doc, from, to, options, type) {
|
|
if (options && options.shared) return markTextShared(doc, from, to, options, type);
|
|
if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type);
|
|
|
|
var marker = new TextMarker(doc, type);
|
|
if (type == "range" && !posLess(from, to)) return marker;
|
|
if (options) copyObj(options, marker);
|
|
if (marker.replacedWith) {
|
|
marker.collapsed = true;
|
|
marker.replacedWith = elt("span", [marker.replacedWith], "CodeMirror-widget");
|
|
}
|
|
if (marker.collapsed) sawCollapsedSpans = true;
|
|
|
|
var curLine = from.line, size = 0, collapsedAtStart, collapsedAtEnd, cm = doc.cm, updateMaxLine;
|
|
doc.iter(curLine, to.line + 1, function(line) {
|
|
if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(doc, line) == cm.display.maxLine)
|
|
updateMaxLine = true;
|
|
var span = {from: null, to: null, marker: marker};
|
|
size += line.text.length;
|
|
if (curLine == from.line) {span.from = from.ch; size -= from.ch;}
|
|
if (curLine == to.line) {span.to = to.ch; size -= line.text.length - to.ch;}
|
|
if (marker.collapsed) {
|
|
if (curLine == to.line) collapsedAtEnd = collapsedSpanAt(line, to.ch);
|
|
if (curLine == from.line) collapsedAtStart = collapsedSpanAt(line, from.ch);
|
|
else updateLineHeight(line, 0);
|
|
}
|
|
addMarkedSpan(line, span);
|
|
++curLine;
|
|
});
|
|
if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) {
|
|
if (lineIsHidden(doc, line)) updateLineHeight(line, 0);
|
|
});
|
|
|
|
if (marker.readOnly) {
|
|
sawReadOnlySpans = true;
|
|
if (doc.history.done.length || doc.history.undone.length)
|
|
doc.clearHistory();
|
|
}
|
|
if (marker.collapsed) {
|
|
if (collapsedAtStart != collapsedAtEnd)
|
|
throw new Error("Inserting collapsed marker overlapping an existing one");
|
|
marker.size = size;
|
|
marker.atomic = true;
|
|
}
|
|
if (cm) {
|
|
if (updateMaxLine) cm.curOp.updateMaxLine = true;
|
|
if (marker.className || marker.startStyle || marker.endStyle || marker.collapsed)
|
|
regChange(cm, from.line, to.line + 1);
|
|
if (marker.atomic) reCheckSelection(cm);
|
|
}
|
|
return marker;
|
|
}
|
|
|
|
// SHARED TEXTMARKERS
|
|
|
|
function SharedTextMarker(markers, primary) {
|
|
this.markers = markers;
|
|
this.primary = primary;
|
|
for (var i = 0, me = this; i < markers.length; ++i) {
|
|
markers[i].parent = this;
|
|
on(markers[i], "clear", function(){me.clear();});
|
|
}
|
|
}
|
|
CodeMirror.SharedTextMarker = SharedTextMarker;
|
|
|
|
SharedTextMarker.prototype.clear = function() {
|
|
if (this.explicitlyCleared) return;
|
|
this.explicitlyCleared = true;
|
|
for (var i = 0; i < this.markers.length; ++i)
|
|
this.markers[i].clear();
|
|
signalLater(this, "clear");
|
|
};
|
|
SharedTextMarker.prototype.find = function() {
|
|
return this.primary.find();
|
|
};
|
|
SharedTextMarker.prototype.getOptions = function(copyWidget) {
|
|
var inner = this.primary.getOptions(copyWidget);
|
|
inner.shared = true;
|
|
return inner;
|
|
};
|
|
|
|
function markTextShared(doc, from, to, options, type) {
|
|
options = copyObj(options);
|
|
options.shared = false;
|
|
var markers = [markText(doc, from, to, options, type)], primary = markers[0];
|
|
linkedDocs(doc, function(doc) {
|
|
markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type));
|
|
for (var i = 0; i < doc.linked.length; ++i)
|
|
if (doc.linked[i].isParent) return;
|
|
primary = lst(markers);
|
|
});
|
|
return new SharedTextMarker(markers, primary);
|
|
}
|
|
|
|
// TEXTMARKER SPANS
|
|
|
|
function getMarkedSpanFor(spans, marker) {
|
|
if (spans) for (var i = 0; i < spans.length; ++i) {
|
|
var span = spans[i];
|
|
if (span.marker == marker) return span;
|
|
}
|
|
}
|
|
function removeMarkedSpan(spans, span) {
|
|
for (var r, i = 0; i < spans.length; ++i)
|
|
if (spans[i] != span) (r || (r = [])).push(spans[i]);
|
|
return r;
|
|
}
|
|
function addMarkedSpan(line, span) {
|
|
line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span];
|
|
span.marker.attachLine(line);
|
|
}
|
|
|
|
function markedSpansBefore(old, startCh, isInsert) {
|
|
if (old) for (var i = 0, nw; i < old.length; ++i) {
|
|
var span = old[i], marker = span.marker;
|
|
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh);
|
|
if (startsBefore || marker.type == "bookmark" && span.from == startCh && (!isInsert || !span.marker.insertLeft)) {
|
|
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);
|
|
(nw || (nw = [])).push({from: span.from,
|
|
to: endsAfter ? null : span.to,
|
|
marker: marker});
|
|
}
|
|
}
|
|
return nw;
|
|
}
|
|
|
|
function markedSpansAfter(old, endCh, isInsert) {
|
|
if (old) for (var i = 0, nw; i < old.length; ++i) {
|
|
var span = old[i], marker = span.marker;
|
|
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh);
|
|
if (endsAfter || marker.type == "bookmark" && span.from == endCh && (!isInsert || span.marker.insertLeft)) {
|
|
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);
|
|
(nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh,
|
|
to: span.to == null ? null : span.to - endCh,
|
|
marker: marker});
|
|
}
|
|
}
|
|
return nw;
|
|
}
|
|
|
|
function stretchSpansOverChange(doc, change) {
|
|
var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans;
|
|
var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans;
|
|
if (!oldFirst && !oldLast) return null;
|
|
|
|
var startCh = change.from.ch, endCh = change.to.ch, isInsert = posEq(change.from, change.to);
|
|
// Get the spans that 'stick out' on both sides
|
|
var first = markedSpansBefore(oldFirst, startCh, isInsert);
|
|
var last = markedSpansAfter(oldLast, endCh, isInsert);
|
|
|
|
// Next, merge those two ends
|
|
var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0);
|
|
if (first) {
|
|
// Fix up .to properties of first
|
|
for (var i = 0; i < first.length; ++i) {
|
|
var span = first[i];
|
|
if (span.to == null) {
|
|
var found = getMarkedSpanFor(last, span.marker);
|
|
if (!found) span.to = startCh;
|
|
else if (sameLine) span.to = found.to == null ? null : found.to + offset;
|
|
}
|
|
}
|
|
}
|
|
if (last) {
|
|
// Fix up .from in last (or move them into first in case of sameLine)
|
|
for (var i = 0; i < last.length; ++i) {
|
|
var span = last[i];
|
|
if (span.to != null) span.to += offset;
|
|
if (span.from == null) {
|
|
var found = getMarkedSpanFor(first, span.marker);
|
|
if (!found) {
|
|
span.from = offset;
|
|
if (sameLine) (first || (first = [])).push(span);
|
|
}
|
|
} else {
|
|
span.from += offset;
|
|
if (sameLine) (first || (first = [])).push(span);
|
|
}
|
|
}
|
|
}
|
|
|
|
var newMarkers = [first];
|
|
if (!sameLine) {
|
|
// Fill gap with whole-line-spans
|
|
var gap = change.text.length - 2, gapMarkers;
|
|
if (gap > 0 && first)
|
|
for (var i = 0; i < first.length; ++i)
|
|
if (first[i].to == null)
|
|
(gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker});
|
|
for (var i = 0; i < gap; ++i)
|
|
newMarkers.push(gapMarkers);
|
|
newMarkers.push(last);
|
|
}
|
|
return newMarkers;
|
|
}
|
|
|
|
function mergeOldSpans(doc, change) {
|
|
var old = getOldSpans(doc, change);
|
|
var stretched = stretchSpansOverChange(doc, change);
|
|
if (!old) return stretched;
|
|
if (!stretched) return old;
|
|
|
|
for (var i = 0; i < old.length; ++i) {
|
|
var oldCur = old[i], stretchCur = stretched[i];
|
|
if (oldCur && stretchCur) {
|
|
spans: for (var j = 0; j < stretchCur.length; ++j) {
|
|
var span = stretchCur[j];
|
|
for (var k = 0; k < oldCur.length; ++k)
|
|
if (oldCur[k].marker == span.marker) continue spans;
|
|
oldCur.push(span);
|
|
}
|
|
} else if (stretchCur) {
|
|
old[i] = stretchCur;
|
|
}
|
|
}
|
|
return old;
|
|
}
|
|
|
|
function removeReadOnlyRanges(doc, from, to) {
|
|
var markers = null;
|
|
doc.iter(from.line, to.line + 1, function(line) {
|
|
if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) {
|
|
var mark = line.markedSpans[i].marker;
|
|
if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))
|
|
(markers || (markers = [])).push(mark);
|
|
}
|
|
});
|
|
if (!markers) return null;
|
|
var parts = [{from: from, to: to}];
|
|
for (var i = 0; i < markers.length; ++i) {
|
|
var mk = markers[i], m = mk.find();
|
|
for (var j = 0; j < parts.length; ++j) {
|
|
var p = parts[j];
|
|
if (posLess(p.to, m.from) || posLess(m.to, p.from)) continue;
|
|
var newParts = [j, 1];
|
|
if (posLess(p.from, m.from) || !mk.inclusiveLeft && posEq(p.from, m.from))
|
|
newParts.push({from: p.from, to: m.from});
|
|
if (posLess(m.to, p.to) || !mk.inclusiveRight && posEq(p.to, m.to))
|
|
newParts.push({from: m.to, to: p.to});
|
|
parts.splice.apply(parts, newParts);
|
|
j += newParts.length - 1;
|
|
}
|
|
}
|
|
return parts;
|
|
}
|
|
|
|
function collapsedSpanAt(line, ch) {
|
|
var sps = sawCollapsedSpans && line.markedSpans, found;
|
|
if (sps) for (var sp, i = 0; i < sps.length; ++i) {
|
|
sp = sps[i];
|
|
if (!sp.marker.collapsed) continue;
|
|
if ((sp.from == null || sp.from < ch) &&
|
|
(sp.to == null || sp.to > ch) &&
|
|
(!found || found.width < sp.marker.width))
|
|
found = sp.marker;
|
|
}
|
|
return found;
|
|
}
|
|
function collapsedSpanAtStart(line) { return collapsedSpanAt(line, -1); }
|
|
function collapsedSpanAtEnd(line) { return collapsedSpanAt(line, line.text.length + 1); }
|
|
|
|
function visualLine(doc, line) {
|
|
var merged;
|
|
while (merged = collapsedSpanAtStart(line))
|
|
line = getLine(doc, merged.find().from.line);
|
|
return line;
|
|
}
|
|
|
|
function lineIsHidden(doc, line) {
|
|
var sps = sawCollapsedSpans && line.markedSpans;
|
|
if (sps) for (var sp, i = 0; i < sps.length; ++i) {
|
|
sp = sps[i];
|
|
if (!sp.marker.collapsed) continue;
|
|
if (sp.from == null) return true;
|
|
if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp))
|
|
return true;
|
|
}
|
|
}
|
|
function lineIsHiddenInner(doc, line, span) {
|
|
if (span.to == null) {
|
|
var end = span.marker.find().to, endLine = getLine(doc, end.line);
|
|
return lineIsHiddenInner(doc, endLine, getMarkedSpanFor(endLine.markedSpans, span.marker));
|
|
}
|
|
if (span.marker.inclusiveRight && span.to == line.text.length)
|
|
return true;
|
|
for (var sp, i = 0; i < line.markedSpans.length; ++i) {
|
|
sp = line.markedSpans[i];
|
|
if (sp.marker.collapsed && sp.from == span.to &&
|
|
(sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
|
|
lineIsHiddenInner(doc, line, sp)) return true;
|
|
}
|
|
}
|
|
|
|
function detachMarkedSpans(line) {
|
|
var spans = line.markedSpans;
|
|
if (!spans) return;
|
|
for (var i = 0; i < spans.length; ++i)
|
|
spans[i].marker.detachLine(line);
|
|
line.markedSpans = null;
|
|
}
|
|
|
|
function attachMarkedSpans(line, spans) {
|
|
if (!spans) return;
|
|
for (var i = 0; i < spans.length; ++i)
|
|
spans[i].marker.attachLine(line);
|
|
line.markedSpans = spans;
|
|
}
|
|
|
|
// LINE WIDGETS
|
|
|
|
var LineWidget = CodeMirror.LineWidget = function(cm, node, options) {
|
|
for (var opt in options) if (options.hasOwnProperty(opt))
|
|
this[opt] = options[opt];
|
|
this.cm = cm;
|
|
this.node = node;
|
|
};
|
|
function widgetOperation(f) {
|
|
return function() {
|
|
var withOp = !this.cm.curOp;
|
|
if (withOp) startOperation(this.cm);
|
|
try {var result = f.apply(this, arguments);}
|
|
finally {if (withOp) endOperation(this.cm);}
|
|
return result;
|
|
};
|
|
}
|
|
LineWidget.prototype.clear = widgetOperation(function() {
|
|
var ws = this.line.widgets, no = lineNo(this.line);
|
|
if (no == null || !ws) return;
|
|
for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1);
|
|
if (!ws.length) this.line.widgets = null;
|
|
updateLineHeight(this.line, Math.max(0, this.line.height - widgetHeight(this)));
|
|
regChange(this.cm, no, no + 1);
|
|
});
|
|
LineWidget.prototype.changed = widgetOperation(function() {
|
|
var oldH = this.height;
|
|
this.height = null;
|
|
var diff = widgetHeight(this) - oldH;
|
|
if (!diff) return;
|
|
updateLineHeight(this.line, this.line.height + diff);
|
|
var no = lineNo(this.line);
|
|
regChange(this.cm, no, no + 1);
|
|
});
|
|
|
|
function widgetHeight(widget) {
|
|
if (widget.height != null) return widget.height;
|
|
if (!widget.node.parentNode || widget.node.parentNode.nodeType != 1)
|
|
removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, "position: relative"));
|
|
return widget.height = widget.node.offsetHeight;
|
|
}
|
|
|
|
function addLineWidget(cm, handle, node, options) {
|
|
var widget = new LineWidget(cm, node, options);
|
|
if (widget.noHScroll) cm.display.alignWidgets = true;
|
|
changeLine(cm, handle, function(line) {
|
|
(line.widgets || (line.widgets = [])).push(widget);
|
|
widget.line = line;
|
|
if (!lineIsHidden(cm.doc, line) || widget.showIfHidden) {
|
|
var aboveVisible = heightAtLine(cm, line) < cm.display.scroller.scrollTop;
|
|
updateLineHeight(line, line.height + widgetHeight(widget));
|
|
if (aboveVisible)
|
|
cm.curOp.updateScrollPos = {scrollTop: cm.doc.scrollTop + widget.height,
|
|
scrollLeft: cm.doc.scrollLeft};
|
|
}
|
|
return true;
|
|
});
|
|
return widget;
|
|
}
|
|
|
|
// LINE DATA STRUCTURE
|
|
|
|
// Line objects. These hold state related to a line, including
|
|
// highlighting info (the styles array).
|
|
function makeLine(text, markedSpans, estimateHeight) {
|
|
var line = {text: text};
|
|
attachMarkedSpans(line, markedSpans);
|
|
line.height = estimateHeight ? estimateHeight(line) : 1;
|
|
return line;
|
|
}
|
|
|
|
function updateLine(line, text, markedSpans, estimateHeight) {
|
|
line.text = text;
|
|
if (line.stateAfter) line.stateAfter = null;
|
|
if (line.styles) line.styles = null;
|
|
if (line.order != null) line.order = null;
|
|
detachMarkedSpans(line);
|
|
attachMarkedSpans(line, markedSpans);
|
|
var estHeight = estimateHeight ? estimateHeight(line) : 1;
|
|
if (estHeight != line.height) updateLineHeight(line, estHeight);
|
|
signalLater(line, "change");
|
|
}
|
|
|
|
function cleanUpLine(line) {
|
|
line.parent = null;
|
|
detachMarkedSpans(line);
|
|
}
|
|
|
|
// Run the given mode's parser over a line, update the styles
|
|
// array, which contains alternating fragments of text and CSS
|
|
// classes.
|
|
function runMode(cm, text, mode, state, f) {
|
|
var flattenSpans = cm.options.flattenSpans;
|
|
var curText = "", curStyle = null;
|
|
var stream = new StringStream(text, cm.options.tabSize);
|
|
if (text == "" && mode.blankLine) mode.blankLine(state);
|
|
while (!stream.eol()) {
|
|
var style = mode.token(stream, state);
|
|
if (stream.pos > 5000) {
|
|
flattenSpans = false;
|
|
// Webkit seems to refuse to render text nodes longer than 57444 characters
|
|
stream.pos = Math.min(text.length, stream.start + 50000);
|
|
style = null;
|
|
}
|
|
var substr = stream.current();
|
|
stream.start = stream.pos;
|
|
if (!flattenSpans || curStyle != style) {
|
|
if (curText) f(curText, curStyle);
|
|
curText = substr; curStyle = style;
|
|
} else curText = curText + substr;
|
|
}
|
|
if (curText) f(curText, curStyle);
|
|
}
|
|
|
|
function highlightLine(cm, line, state) {
|
|
// A styles array always starts with a number identifying the
|
|
// mode/overlays that it is based on (for easy invalidation).
|
|
var st = [cm.state.modeGen];
|
|
// Compute the base array of styles
|
|
runMode(cm, line.text, cm.doc.mode, state, function(txt, style) {st.push(txt, style);});
|
|
|
|
// Run overlays, adjust style array.
|
|
for (var o = 0; o < cm.state.overlays.length; ++o) {
|
|
var overlay = cm.state.overlays[o], i = 1;
|
|
runMode(cm, line.text, overlay.mode, true, function(txt, style) {
|
|
var start = i, len = txt.length;
|
|
// Ensure there's a token end at the current position, and that i points at it
|
|
while (len) {
|
|
var cur = st[i], len_ = cur.length;
|
|
if (len_ <= len) {
|
|
len -= len_;
|
|
} else {
|
|
st.splice(i, 1, cur.slice(0, len), st[i+1], cur.slice(len));
|
|
len = 0;
|
|
}
|
|
i += 2;
|
|
}
|
|
if (!style) return;
|
|
if (overlay.opaque) {
|
|
st.splice(start, i - start, txt, style);
|
|
i = start + 2;
|
|
} else {
|
|
for (; start < i; start += 2) {
|
|
var cur = st[start+1];
|
|
st[start+1] = cur ? cur + " " + style : style;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return st;
|
|
}
|
|
|
|
function getLineStyles(cm, line) {
|
|
if (!line.styles || line.styles[0] != cm.state.modeGen)
|
|
line.styles = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line)));
|
|
return line.styles;
|
|
}
|
|
|
|
// Lightweight form of highlight -- proceed over this line and
|
|
// update state, but don't save a style array.
|
|
function processLine(cm, line, state) {
|
|
var mode = cm.doc.mode;
|
|
var stream = new StringStream(line.text, cm.options.tabSize);
|
|
if (line.text == "" && mode.blankLine) mode.blankLine(state);
|
|
while (!stream.eol() && stream.pos <= 5000) {
|
|
mode.token(stream, state);
|
|
stream.start = stream.pos;
|
|
}
|
|
}
|
|
|
|
var styleToClassCache = {};
|
|
function styleToClass(style) {
|
|
if (!style) return null;
|
|
return styleToClassCache[style] ||
|
|
(styleToClassCache[style] = "cm-" + style.replace(/ +/g, " cm-"));
|
|
}
|
|
|
|
function lineContent(cm, realLine, measure) {
|
|
var merged, line = realLine, lineBefore, sawBefore, simple = true;
|
|
while (merged = collapsedSpanAtStart(line)) {
|
|
simple = false;
|
|
line = getLine(cm.doc, merged.find().from.line);
|
|
if (!lineBefore) lineBefore = line;
|
|
}
|
|
|
|
var builder = {pre: elt("pre"), col: 0, pos: 0, display: !measure,
|
|
measure: null, addedOne: false, cm: cm};
|
|
if (line.textClass) builder.pre.className = line.textClass;
|
|
|
|
do {
|
|
builder.measure = line == realLine && measure;
|
|
builder.pos = 0;
|
|
builder.addToken = builder.measure ? buildTokenMeasure : buildToken;
|
|
if (measure && sawBefore && line != realLine && !builder.addedOne) {
|
|
measure[0] = builder.pre.appendChild(zeroWidthElement(cm.display.measure));
|
|
builder.addedOne = true;
|
|
}
|
|
var next = insertLineContent(line, builder, getLineStyles(cm, line));
|
|
sawBefore = line == lineBefore;
|
|
if (next) {
|
|
line = getLine(cm.doc, next.to.line);
|
|
simple = false;
|
|
}
|
|
} while (next);
|
|
|
|
if (measure && !builder.addedOne)
|
|
measure[0] = builder.pre.appendChild(simple ? elt("span", "\u00a0") : zeroWidthElement(cm.display.measure));
|
|
if (!builder.pre.firstChild && !lineIsHidden(cm.doc, realLine))
|
|
builder.pre.appendChild(document.createTextNode("\u00a0"));
|
|
|
|
var order;
|
|
// Work around problem with the reported dimensions of single-char
|
|
// direction spans on IE (issue #1129). See also the comment in
|
|
// cursorCoords.
|
|
if (measure && ie && (order = getOrder(line))) {
|
|
var l = order.length - 1;
|
|
if (order[l].from == order[l].to) --l;
|
|
var last = order[l], prev = order[l - 1];
|
|
if (last.from + 1 == last.to && prev && last.level < prev.level) {
|
|
var span = measure[builder.pos - 1];
|
|
if (span) span.parentNode.insertBefore(span.measureRight = zeroWidthElement(cm.display.measure),
|
|
span.nextSibling);
|
|
}
|
|
}
|
|
|
|
return builder.pre;
|
|
}
|
|
|
|
var tokenSpecialChars = /[\t\u0000-\u0019\u200b\u2028\u2029\uFEFF]/g;
|
|
function buildToken(builder, text, style, startStyle, endStyle) {
|
|
if (!text) return;
|
|
if (!tokenSpecialChars.test(text)) {
|
|
builder.col += text.length;
|
|
var content = document.createTextNode(text);
|
|
} else {
|
|
var content = document.createDocumentFragment(), pos = 0;
|
|
while (true) {
|
|
tokenSpecialChars.lastIndex = pos;
|
|
var m = tokenSpecialChars.exec(text);
|
|
var skipped = m ? m.index - pos : text.length - pos;
|
|
if (skipped) {
|
|
content.appendChild(document.createTextNode(text.slice(pos, pos + skipped)));
|
|
builder.col += skipped;
|
|
}
|
|
if (!m) break;
|
|
pos += skipped + 1;
|
|
if (m[0] == "\t") {
|
|
var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize;
|
|
content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"));
|
|
builder.col += tabWidth;
|
|
} else {
|
|
var token = elt("span", "\u2022", "cm-invalidchar");
|
|
token.title = "\\u" + m[0].charCodeAt(0).toString(16);
|
|
content.appendChild(token);
|
|
builder.col += 1;
|
|
}
|
|
}
|
|
}
|
|
if (style || startStyle || endStyle || builder.measure) {
|
|
var fullStyle = style || "";
|
|
if (startStyle) fullStyle += startStyle;
|
|
if (endStyle) fullStyle += endStyle;
|
|
return builder.pre.appendChild(elt("span", [content], fullStyle));
|
|
}
|
|
builder.pre.appendChild(content);
|
|
}
|
|
|
|
function buildTokenMeasure(builder, text, style, startStyle, endStyle) {
|
|
for (var i = 0; i < text.length; ++i) {
|
|
var ch = text.charAt(i), start = i == 0;
|
|
if (ch >= "\ud800" && ch < "\udbff" && i < text.length - 1) {
|
|
ch = text.slice(i, i + 2);
|
|
++i;
|
|
} else if (i && builder.cm.options.lineWrapping &&
|
|
spanAffectsWrapping.test(text.slice(i - 1, i + 1))) {
|
|
builder.pre.appendChild(elt("wbr"));
|
|
}
|
|
builder.measure[builder.pos] =
|
|
buildToken(builder, ch, style,
|
|
start && startStyle, i == text.length - 1 && endStyle);
|
|
builder.pos += ch.length;
|
|
}
|
|
if (text.length) builder.addedOne = true;
|
|
}
|
|
|
|
function buildCollapsedSpan(builder, size, widget) {
|
|
if (widget) {
|
|
if (!builder.display) widget = widget.cloneNode(true);
|
|
builder.pre.appendChild(widget);
|
|
if (builder.measure && size) {
|
|
builder.measure[builder.pos] = widget;
|
|
builder.addedOne = true;
|
|
}
|
|
}
|
|
builder.pos += size;
|
|
}
|
|
|
|
// Outputs a number of spans to make up a line, taking highlighting
|
|
// and marked text into account.
|
|
function insertLineContent(line, builder, styles) {
|
|
var spans = line.markedSpans;
|
|
if (!spans) {
|
|
for (var i = 1; i < styles.length; i+=2)
|
|
builder.addToken(builder, styles[i], styleToClass(styles[i+1]));
|
|
return;
|
|
}
|
|
|
|
var allText = line.text, len = allText.length;
|
|
var pos = 0, i = 1, text = "", style;
|
|
var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, collapsed;
|
|
for (;;) {
|
|
if (nextChange == pos) { // Update current marker set
|
|
spanStyle = spanEndStyle = spanStartStyle = "";
|
|
collapsed = null; nextChange = Infinity;
|
|
var foundBookmark = null;
|
|
for (var j = 0; j < spans.length; ++j) {
|
|
var sp = spans[j], m = sp.marker;
|
|
if (sp.from <= pos && (sp.to == null || sp.to > pos)) {
|
|
if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; }
|
|
if (m.className) spanStyle += " " + m.className;
|
|
if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle;
|
|
if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle;
|
|
if (m.collapsed && (!collapsed || collapsed.marker.width < m.width))
|
|
collapsed = sp;
|
|
} else if (sp.from > pos && nextChange > sp.from) {
|
|
nextChange = sp.from;
|
|
}
|
|
if (m.type == "bookmark" && sp.from == pos && m.replacedWith)
|
|
foundBookmark = m.replacedWith;
|
|
}
|
|
if (collapsed && (collapsed.from || 0) == pos) {
|
|
buildCollapsedSpan(builder, (collapsed.to == null ? len : collapsed.to) - pos,
|
|
collapsed.from != null && collapsed.marker.replacedWith);
|
|
if (collapsed.to == null) return collapsed.marker.find();
|
|
}
|
|
if (foundBookmark && !collapsed) buildCollapsedSpan(builder, 0, foundBookmark);
|
|
}
|
|
if (pos >= len) break;
|
|
|
|
var upto = Math.min(len, nextChange);
|
|
while (true) {
|
|
if (text) {
|
|
var end = pos + text.length;
|
|
if (!collapsed) {
|
|
var tokenText = end > upto ? text.slice(0, upto - pos) : text;
|
|
builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle,
|
|
spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "");
|
|
}
|
|
if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
|
|
pos = end;
|
|
spanStartStyle = "";
|
|
}
|
|
text = styles[i++]; style = styleToClass(styles[i++]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// DOCUMENT DATA STRUCTURE
|
|
|
|
function updateDoc(doc, change, markedSpans, selAfter, estimateHeight) {
|
|
function spansFor(n) {return markedSpans ? markedSpans[n] : null;}
|
|
|
|
var from = change.from, to = change.to, text = change.text;
|
|
var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line);
|
|
var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line;
|
|
|
|
// First adjust the line structure
|
|
if (from.ch == 0 && to.ch == 0 && lastText == "") {
|
|
// This is a whole-line replace. Treated specially to make
|
|
// sure line objects move the way they are supposed to.
|
|
for (var i = 0, e = text.length - 1, added = []; i < e; ++i)
|
|
added.push(makeLine(text[i], spansFor(i), estimateHeight));
|
|
updateLine(lastLine, lastLine.text, lastSpans, estimateHeight);
|
|
if (nlines) doc.remove(from.line, nlines);
|
|
if (added.length) doc.insert(from.line, added);
|
|
} else if (firstLine == lastLine) {
|
|
if (text.length == 1) {
|
|
updateLine(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch),
|
|
lastSpans, estimateHeight);
|
|
} else {
|
|
for (var added = [], i = 1, e = text.length - 1; i < e; ++i)
|
|
added.push(makeLine(text[i], spansFor(i), estimateHeight));
|
|
added.push(makeLine(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight));
|
|
updateLine(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0), estimateHeight);
|
|
doc.insert(from.line + 1, added);
|
|
}
|
|
} else if (text.length == 1) {
|
|
updateLine(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch),
|
|
spansFor(0), estimateHeight);
|
|
doc.remove(from.line + 1, nlines);
|
|
} else {
|
|
updateLine(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0), estimateHeight);
|
|
updateLine(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans, estimateHeight);
|
|
for (var i = 1, e = text.length - 1, added = []; i < e; ++i)
|
|
added.push(makeLine(text[i], spansFor(i), estimateHeight));
|
|
if (nlines > 1) doc.remove(from.line + 1, nlines - 1);
|
|
doc.insert(from.line + 1, added);
|
|
}
|
|
|
|
signalLater(doc, "change", doc, change);
|
|
setSelection(doc, selAfter.anchor, selAfter.head, null, true);
|
|
}
|
|
|
|
function LeafChunk(lines) {
|
|
this.lines = lines;
|
|
this.parent = null;
|
|
for (var i = 0, e = lines.length, height = 0; i < e; ++i) {
|
|
lines[i].parent = this;
|
|
height += lines[i].height;
|
|
}
|
|
this.height = height;
|
|
}
|
|
|
|
LeafChunk.prototype = {
|
|
chunkSize: function() { return this.lines.length; },
|
|
removeInner: function(at, n) {
|
|
for (var i = at, e = at + n; i < e; ++i) {
|
|
var line = this.lines[i];
|
|
this.height -= line.height;
|
|
cleanUpLine(line);
|
|
signalLater(line, "delete");
|
|
}
|
|
this.lines.splice(at, n);
|
|
},
|
|
collapse: function(lines) {
|
|
lines.splice.apply(lines, [lines.length, 0].concat(this.lines));
|
|
},
|
|
insertInner: function(at, lines, height) {
|
|
this.height += height;
|
|
this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at));
|
|
for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this;
|
|
},
|
|
iterN: function(at, n, op) {
|
|
for (var e = at + n; at < e; ++at)
|
|
if (op(this.lines[at])) return true;
|
|
}
|
|
};
|
|
|
|
function BranchChunk(children) {
|
|
this.children = children;
|
|
var size = 0, height = 0;
|
|
for (var i = 0, e = children.length; i < e; ++i) {
|
|
var ch = children[i];
|
|
size += ch.chunkSize(); height += ch.height;
|
|
ch.parent = this;
|
|
}
|
|
this.size = size;
|
|
this.height = height;
|
|
this.parent = null;
|
|
}
|
|
|
|
BranchChunk.prototype = {
|
|
chunkSize: function() { return this.size; },
|
|
removeInner: function(at, n) {
|
|
this.size -= n;
|
|
for (var i = 0; i < this.children.length; ++i) {
|
|
var child = this.children[i], sz = child.chunkSize();
|
|
if (at < sz) {
|
|
var rm = Math.min(n, sz - at), oldHeight = child.height;
|
|
child.removeInner(at, rm);
|
|
this.height -= oldHeight - child.height;
|
|
if (sz == rm) { this.children.splice(i--, 1); child.parent = null; }
|
|
if ((n -= rm) == 0) break;
|
|
at = 0;
|
|
} else at -= sz;
|
|
}
|
|
if (this.size - n < 25) {
|
|
var lines = [];
|
|
this.collapse(lines);
|
|
this.children = [new LeafChunk(lines)];
|
|
this.children[0].parent = this;
|
|
}
|
|
},
|
|
collapse: function(lines) {
|
|
for (var i = 0, e = this.children.length; i < e; ++i) this.children[i].collapse(lines);
|
|
},
|
|
insertInner: function(at, lines, height) {
|
|
this.size += lines.length;
|
|
this.height += height;
|
|
for (var i = 0, e = this.children.length; i < e; ++i) {
|
|
var child = this.children[i], sz = child.chunkSize();
|
|
if (at <= sz) {
|
|
child.insertInner(at, lines, height);
|
|
if (child.lines && child.lines.length > 50) {
|
|
while (child.lines.length > 50) {
|
|
var spilled = child.lines.splice(child.lines.length - 25, 25);
|
|
var newleaf = new LeafChunk(spilled);
|
|
child.height -= newleaf.height;
|
|
this.children.splice(i + 1, 0, newleaf);
|
|
newleaf.parent = this;
|
|
}
|
|
this.maybeSpill();
|
|
}
|
|
break;
|
|
}
|
|
at -= sz;
|
|
}
|
|
},
|
|
maybeSpill: function() {
|
|
if (this.children.length <= 10) return;
|
|
var me = this;
|
|
do {
|
|
var spilled = me.children.splice(me.children.length - 5, 5);
|
|
var sibling = new BranchChunk(spilled);
|
|
if (!me.parent) { // Become the parent node
|
|
var copy = new BranchChunk(me.children);
|
|
copy.parent = me;
|
|
me.children = [copy, sibling];
|
|
me = copy;
|
|
} else {
|
|
me.size -= sibling.size;
|
|
me.height -= sibling.height;
|
|
var myIndex = indexOf(me.parent.children, me);
|
|
me.parent.children.splice(myIndex + 1, 0, sibling);
|
|
}
|
|
sibling.parent = me.parent;
|
|
} while (me.children.length > 10);
|
|
me.parent.maybeSpill();
|
|
},
|
|
iterN: function(at, n, op) {
|
|
for (var i = 0, e = this.children.length; i < e; ++i) {
|
|
var child = this.children[i], sz = child.chunkSize();
|
|
if (at < sz) {
|
|
var used = Math.min(n, sz - at);
|
|
if (child.iterN(at, used, op)) return true;
|
|
if ((n -= used) == 0) break;
|
|
at = 0;
|
|
} else at -= sz;
|
|
}
|
|
}
|
|
};
|
|
|
|
var nextDocId = 0;
|
|
var Doc = CodeMirror.Doc = function(text, mode, firstLine) {
|
|
if (!(this instanceof Doc)) return new Doc(text, mode, firstLine);
|
|
if (firstLine == null) firstLine = 0;
|
|
|
|
BranchChunk.call(this, [new LeafChunk([makeLine("", null)])]);
|
|
this.first = firstLine;
|
|
this.scrollTop = this.scrollLeft = 0;
|
|
this.cantEdit = false;
|
|
this.history = makeHistory();
|
|
this.frontier = firstLine;
|
|
var start = Pos(firstLine, 0);
|
|
this.sel = {from: start, to: start, head: start, anchor: start, shift: false, extend: false, goalColumn: null};
|
|
this.id = ++nextDocId;
|
|
this.modeOption = mode;
|
|
|
|
if (typeof text == "string") text = splitLines(text);
|
|
updateDoc(this, {from: start, to: start, text: text}, null, {head: start, anchor: start});
|
|
};
|
|
|
|
Doc.prototype = createObj(BranchChunk.prototype, {
|
|
iter: function(from, to, op) {
|
|
if (op) this.iterN(from - this.first, to - from, op);
|
|
else this.iterN(this.first, this.first + this.size, from);
|
|
},
|
|
|
|
insert: function(at, lines) {
|
|
var height = 0;
|
|
for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height;
|
|
this.insertInner(at - this.first, lines, height);
|
|
},
|
|
remove: function(at, n) { this.removeInner(at - this.first, n); },
|
|
|
|
getValue: function(lineSep) {
|
|
var lines = getLines(this, this.first, this.first + this.size);
|
|
if (lineSep === false) return lines;
|
|
return lines.join(lineSep || "\n");
|
|
},
|
|
setValue: function(code) {
|
|
var top = Pos(this.first, 0), last = this.first + this.size - 1;
|
|
makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length),
|
|
text: splitLines(code), origin: "setValue"},
|
|
{head: top, anchor: top}, true);
|
|
},
|
|
replaceRange: function(code, from, to, origin) {
|
|
from = clipPos(this, from);
|
|
to = to ? clipPos(this, to) : from;
|
|
replaceRange(this, code, from, to, origin);
|
|
},
|
|
getRange: function(from, to, lineSep) {
|
|
var lines = getBetween(this, clipPos(this, from), clipPos(this, to));
|
|
if (lineSep === false) return lines;
|
|
return lines.join(lineSep || "\n");
|
|
},
|
|
|
|
getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;},
|
|
setLine: function(line, text) {
|
|
if (isLine(this, line))
|
|
replaceRange(this, text, Pos(line, 0), clipPos(this, Pos(line)));
|
|
},
|
|
removeLine: function(line) {
|
|
if (isLine(this, line))
|
|
replaceRange(this, "", Pos(line, 0), clipPos(this, Pos(line + 1, 0)));
|
|
},
|
|
|
|
getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);},
|
|
getLineNumber: function(line) {return lineNo(line);},
|
|
|
|
lineCount: function() {return this.size;},
|
|
firstLine: function() {return this.first;},
|
|
lastLine: function() {return this.first + this.size - 1;},
|
|
|
|
clipPos: function(pos) {return clipPos(this, pos);},
|
|
|
|
getCursor: function(start) {
|
|
var sel = this.sel, pos;
|
|
if (start == null || start == "head") pos = sel.head;
|
|
else if (start == "anchor") pos = sel.anchor;
|
|
else if (start == "end" || start === false) pos = sel.to;
|
|
else pos = sel.from;
|
|
return copyPos(pos);
|
|
},
|
|
somethingSelected: function() {return !posEq(this.sel.head, this.sel.anchor);},
|
|
|
|
setCursor: docOperation(function(line, ch, extend) {
|
|
var pos = clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line);
|
|
if (extend) extendSelection(this, pos);
|
|
else setSelection(this, pos, pos);
|
|
}),
|
|
setSelection: docOperation(function(anchor, head) {
|
|
setSelection(this, clipPos(this, anchor), clipPos(this, head || anchor));
|
|
}),
|
|
extendSelection: docOperation(function(from, to) {
|
|
extendSelection(this, clipPos(this, from), to && clipPos(this, to));
|
|
}),
|
|
|
|
getSelection: function(lineSep) {return this.getRange(this.sel.from, this.sel.to, lineSep);},
|
|
replaceSelection: function(code, collapse, origin) {
|
|
makeChange(this, {from: this.sel.from, to: this.sel.to, text: splitLines(code), origin: origin}, collapse || "around");
|
|
},
|
|
undo: docOperation(function() {makeChangeFromHistory(this, "undo");}),
|
|
redo: docOperation(function() {makeChangeFromHistory(this, "redo");}),
|
|
|
|
setExtending: function(val) {this.sel.extend = val;},
|
|
|
|
historySize: function() {
|
|
var hist = this.history;
|
|
return {undo: hist.done.length, redo: hist.undone.length};
|
|
},
|
|
clearHistory: function() {this.history = makeHistory();},
|
|
|
|
markClean: function() {
|
|
this.history.dirtyCounter = 0;
|
|
this.history.lastOp = this.history.lastOrigin = null;
|
|
},
|
|
isClean: function () {return this.history.dirtyCounter == 0;},
|
|
|
|
getHistory: function() {
|
|
return {done: copyHistoryArray(this.history.done),
|
|
undone: copyHistoryArray(this.history.undone)};
|
|
},
|
|
setHistory: function(histData) {
|
|
var hist = this.history = makeHistory();
|
|
hist.done = histData.done.slice(0);
|
|
hist.undone = histData.undone.slice(0);
|
|
},
|
|
|
|
markText: function(from, to, options) {
|
|
return markText(this, clipPos(this, from), clipPos(this, to), options, "range");
|
|
},
|
|
setBookmark: function(pos, options) {
|
|
var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),
|
|
insertLeft: options && options.insertLeft};
|
|
pos = clipPos(this, pos);
|
|
return markText(this, pos, pos, realOpts, "bookmark");
|
|
},
|
|
findMarksAt: function(pos) {
|
|
pos = clipPos(this, pos);
|
|
var markers = [], spans = getLine(this, pos.line).markedSpans;
|
|
if (spans) for (var i = 0; i < spans.length; ++i) {
|
|
var span = spans[i];
|
|
if ((span.from == null || span.from <= pos.ch) &&
|
|
(span.to == null || span.to >= pos.ch))
|
|
markers.push(span.marker.parent || span.marker);
|
|
}
|
|
return markers;
|
|
},
|
|
getAllMarks: function() {
|
|
var markers = [];
|
|
this.iter(function(line) {
|
|
var sps = line.markedSpans;
|
|
if (sps) for (var i = 0; i < sps.length; ++i)
|
|
if (sps[i].from != null) markers.push(sps[i].marker);
|
|
});
|
|
return markers;
|
|
},
|
|
|
|
posFromIndex: function(off) {
|
|
var ch, lineNo = this.first;
|
|
this.iter(function(line) {
|
|
var sz = line.text.length + 1;
|
|
if (sz > off) { ch = off; return true; }
|
|
off -= sz;
|
|
++lineNo;
|
|
});
|
|
return clipPos(this, Pos(lineNo, ch));
|
|
},
|
|
indexFromPos: function (coords) {
|
|
coords = clipPos(this, coords);
|
|
var index = coords.ch;
|
|
if (coords.line < this.first || coords.ch < 0) return 0;
|
|
this.iter(this.first, coords.line, function (line) {
|
|
index += line.text.length + 1;
|
|
});
|
|
return index;
|
|
},
|
|
|
|
copy: function(copyHistory) {
|
|
var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first);
|
|
doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft;
|
|
doc.sel = {from: this.sel.from, to: this.sel.to, head: this.sel.head, anchor: this.sel.anchor,
|
|
shift: this.sel.shift, extend: false, goalColumn: this.sel.goalColumn};
|
|
if (copyHistory) {
|
|
doc.history.undoDepth = this.history.undoDepth;
|
|
doc.setHistory(this.getHistory());
|
|
}
|
|
return doc;
|
|
},
|
|
|
|
linkedDoc: function(options) {
|
|
if (!options) options = {};
|
|
var from = this.first, to = this.first + this.size;
|
|
if (options.from != null && options.from > from) from = options.from;
|
|
if (options.to != null && options.to < to) to = options.to;
|
|
var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from);
|
|
if (options.sharedHist) copy.history = this.history;
|
|
(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist});
|
|
copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}];
|
|
return copy;
|
|
},
|
|
unlinkDoc: function(other) {
|
|
if (other instanceof CodeMirror) other = other.doc;
|
|
if (this.linked) for (var i = 0; i < this.linked.length; ++i) {
|
|
var link = this.linked[i];
|
|
if (link.doc != other) continue;
|
|
this.linked.splice(i, 1);
|
|
other.unlinkDoc(this);
|
|
break;
|
|
}
|
|
// If the histories were shared, split them again
|
|
if (other.history == this.history) {
|
|
var splitIds = [other.id];
|
|
linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true);
|
|
other.history = makeHistory();
|
|
other.history.done = copyHistoryArray(this.history.done, splitIds);
|
|
other.history.undone = copyHistoryArray(this.history.undone, splitIds);
|
|
}
|
|
},
|
|
iterLinkedDocs: function(f) {linkedDocs(this, f);},
|
|
|
|
getMode: function() {return this.mode;},
|
|
getEditor: function() {return this.cm;}
|
|
});
|
|
|
|
Doc.prototype.eachLine = Doc.prototype.iter;
|
|
|
|
// The Doc methods that should be available on CodeMirror instances
|
|
var dontDelegate = "iter insert remove copy getEditor".split(" ");
|
|
for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0)
|
|
CodeMirror.prototype[prop] = (function(method) {
|
|
return function() {return method.apply(this.doc, arguments);};
|
|
})(Doc.prototype[prop]);
|
|
|
|
function linkedDocs(doc, f, sharedHistOnly) {
|
|
function propagate(doc, skip, sharedHist) {
|
|
if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) {
|
|
var rel = doc.linked[i];
|
|
if (rel.doc == skip) continue;
|
|
var shared = sharedHist && rel.sharedHist;
|
|
if (sharedHistOnly && !shared) continue;
|
|
f(rel.doc, shared);
|
|
propagate(rel.doc, doc, shared);
|
|
}
|
|
}
|
|
propagate(doc, null, true);
|
|
}
|
|
|
|
function attachDoc(cm, doc) {
|
|
if (doc.cm) throw new Error("This document is already in use.");
|
|
cm.doc = doc;
|
|
doc.cm = cm;
|
|
estimateLineHeights(cm);
|
|
loadMode(cm);
|
|
if (!cm.options.lineWrapping) computeMaxLength(cm);
|
|
cm.options.mode = doc.modeOption;
|
|
regChange(cm);
|
|
}
|
|
|
|
// LINE UTILITIES
|
|
|
|
function getLine(chunk, n) {
|
|
n -= chunk.first;
|
|
while (!chunk.lines) {
|
|
for (var i = 0;; ++i) {
|
|
var child = chunk.children[i], sz = child.chunkSize();
|
|
if (n < sz) { chunk = child; break; }
|
|
n -= sz;
|
|
}
|
|
}
|
|
return chunk.lines[n];
|
|
}
|
|
|
|
function getBetween(doc, start, end) {
|
|
var out = [], n = start.line;
|
|
doc.iter(start.line, end.line + 1, function(line) {
|
|
var text = line.text;
|
|
if (n == end.line) text = text.slice(0, end.ch);
|
|
if (n == start.line) text = text.slice(start.ch);
|
|
out.push(text);
|
|
++n;
|
|
});
|
|
return out;
|
|
}
|
|
function getLines(doc, from, to) {
|
|
var out = [];
|
|
doc.iter(from, to, function(line) { out.push(line.text); });
|
|
return out;
|
|
}
|
|
|
|
function updateLineHeight(line, height) {
|
|
var diff = height - line.height;
|
|
for (var n = line; n; n = n.parent) n.height += diff;
|
|
}
|
|
|
|
function lineNo(line) {
|
|
if (line.parent == null) return null;
|
|
var cur = line.parent, no = indexOf(cur.lines, line);
|
|
for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {
|
|
for (var i = 0;; ++i) {
|
|
if (chunk.children[i] == cur) break;
|
|
no += chunk.children[i].chunkSize();
|
|
}
|
|
}
|
|
return no + cur.first;
|
|
}
|
|
|
|
function lineAtHeight(chunk, h) {
|
|
var n = chunk.first;
|
|
outer: do {
|
|
for (var i = 0, e = chunk.children.length; i < e; ++i) {
|
|
var child = chunk.children[i], ch = child.height;
|
|
if (h < ch) { chunk = child; continue outer; }
|
|
h -= ch;
|
|
n += child.chunkSize();
|
|
}
|
|
return n;
|
|
} while (!chunk.lines);
|
|
for (var i = 0, e = chunk.lines.length; i < e; ++i) {
|
|
var line = chunk.lines[i], lh = line.height;
|
|
if (h < lh) break;
|
|
h -= lh;
|
|
}
|
|
return n + i;
|
|
}
|
|
|
|
function heightAtLine(cm, lineObj) {
|
|
lineObj = visualLine(cm.doc, lineObj);
|
|
|
|
var h = 0, chunk = lineObj.parent;
|
|
for (var i = 0; i < chunk.lines.length; ++i) {
|
|
var line = chunk.lines[i];
|
|
if (line == lineObj) break;
|
|
else h += line.height;
|
|
}
|
|
for (var p = chunk.parent; p; chunk = p, p = chunk.parent) {
|
|
for (var i = 0; i < p.children.length; ++i) {
|
|
var cur = p.children[i];
|
|
if (cur == chunk) break;
|
|
else h += cur.height;
|
|
}
|
|
}
|
|
return h;
|
|
}
|
|
|
|
function getOrder(line) {
|
|
var order = line.order;
|
|
if (order == null) order = line.order = bidiOrdering(line.text);
|
|
return order;
|
|
}
|
|
|
|
// HISTORY
|
|
|
|
function makeHistory() {
|
|
return {
|
|
// Arrays of history events. Doing something adds an event to
|
|
// done and clears undo. Undoing moves events from done to
|
|
// undone, redoing moves them in the other direction.
|
|
done: [], undone: [], undoDepth: Infinity,
|
|
// Used to track when changes can be merged into a single undo
|
|
// event
|
|
lastTime: 0, lastOp: null, lastOrigin: null,
|
|
// Used by the isClean() method
|
|
dirtyCounter: 0
|
|
};
|
|
}
|
|
|
|
function attachLocalSpans(doc, change, from, to) {
|
|
var existing = change["spans_" + doc.id], n = 0;
|
|
doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) {
|
|
if (line.markedSpans)
|
|
(existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans;
|
|
++n;
|
|
});
|
|
}
|
|
|
|
function historyChangeFromChange(doc, change) {
|
|
var histChange = {from: change.from, to: changeEnd(change), text: getBetween(doc, change.from, change.to)};
|
|
attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);
|
|
linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true);
|
|
return histChange;
|
|
}
|
|
|
|
function addToHistory(doc, change, selAfter, opId) {
|
|
var hist = doc.history;
|
|
hist.undone.length = 0;
|
|
var time = +new Date, cur = lst(hist.done);
|
|
|
|
if (cur &&
|
|
(hist.lastOp == opId ||
|
|
hist.lastOrigin == change.origin && change.origin &&
|
|
((change.origin.charAt(0) == "+" && hist.lastTime > time - 600) || change.origin.charAt(0) == "*"))) {
|
|
// Merge this change into the last event
|
|
var last = lst(cur.changes);
|
|
if (posEq(change.from, change.to) && posEq(change.from, last.to)) {
|
|
// Optimized case for simple insertion -- don't want to add
|
|
// new changesets for every character typed
|
|
last.to = changeEnd(change);
|
|
} else {
|
|
// Add new sub-event
|
|
cur.changes.push(historyChangeFromChange(doc, change));
|
|
}
|
|
cur.anchorAfter = selAfter.anchor; cur.headAfter = selAfter.head;
|
|
} else {
|
|
// Can not be merged, start a new event.
|
|
cur = {changes: [historyChangeFromChange(doc, change)],
|
|
anchorBefore: doc.sel.anchor, headBefore: doc.sel.head,
|
|
anchorAfter: selAfter.anchor, headAfter: selAfter.head};
|
|
hist.done.push(cur);
|
|
while (hist.done.length > hist.undoDepth)
|
|
hist.done.shift();
|
|
if (hist.dirtyCounter < 0)
|
|
// The user has made a change after undoing past the last clean state.
|
|
// We can never get back to a clean state now until markClean() is called.
|
|
hist.dirtyCounter = NaN;
|
|
else
|
|
hist.dirtyCounter++;
|
|
}
|
|
hist.lastTime = time;
|
|
hist.lastOp = opId;
|
|
hist.lastOrigin = change.origin;
|
|
}
|
|
|
|
function removeClearedSpans(spans) {
|
|
if (!spans) return null;
|
|
for (var i = 0, out; i < spans.length; ++i) {
|
|
if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); }
|
|
else if (out) out.push(spans[i]);
|
|
}
|
|
return !out ? spans : out.length ? out : null;
|
|
}
|
|
|
|
function getOldSpans(doc, change) {
|
|
var found = change["spans_" + doc.id];
|
|
if (!found) return null;
|
|
for (var i = 0, nw = []; i < change.text.length; ++i)
|
|
nw.push(removeClearedSpans(found[i]));
|
|
return nw;
|
|
}
|
|
|
|
// Used both to provide a JSON-safe object in .getHistory, and, when
|
|
// detaching a document, to split the history in two
|
|
function copyHistoryArray(events, newGroup) {
|
|
for (var i = 0, copy = []; i < events.length; ++i) {
|
|
var event = events[i], changes = event.changes, newChanges = [];
|
|
copy.push({changes: newChanges, anchorBefore: event.anchorBefore, headBefore: event.headBefore,
|
|
anchorAfter: event.anchorAfter, headAfter: event.headAfter});
|
|
for (var j = 0; j < changes.length; ++j) {
|
|
var change = changes[j], m;
|
|
newChanges.push({from: change.from, to: change.to, text: change.text});
|
|
if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) {
|
|
if (indexOf(newGroup, Number(m[1])) > -1) {
|
|
lst(newChanges)[prop] = change[prop];
|
|
delete change[prop];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return copy;
|
|
}
|
|
|
|
// Rebasing/resetting history to deal with externally-sourced changes
|
|
|
|
function rebaseHistSel(pos, from, to, diff) {
|
|
if (to < pos.line) {
|
|
pos.line += diff;
|
|
} else if (from < pos.line) {
|
|
pos.line = from;
|
|
pos.ch = 0;
|
|
}
|
|
}
|
|
|
|
// Tries to rebase an array of history events given a change in the
|
|
// document. If the change touches the same lines as the event, the
|
|
// event, and everything 'behind' it, is discarded. If the change is
|
|
// before the event, the event's positions are updated. Uses a
|
|
// copy-on-write scheme for the positions, to avoid having to
|
|
// reallocate them all on every rebase, but also avoid problems with
|
|
// shared position objects being unsafely updated.
|
|
function rebaseHistArray(array, from, to, diff) {
|
|
for (var i = 0; i < array.length; ++i) {
|
|
var sub = array[i], ok = true;
|
|
for (var j = 0; j < sub.changes.length; ++j) {
|
|
var cur = sub.changes[j];
|
|
if (!sub.copied) { cur.from = copyPos(cur.from); cur.to = copyPos(cur.to); }
|
|
if (to < cur.from.line) {
|
|
cur.from.line += diff;
|
|
cur.to.line += diff;
|
|
} else if (from <= cur.to.line) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!sub.copied) {
|
|
sub.anchorBefore = copyPos(sub.anchorBefore); sub.headBefore = copyPos(sub.headBefore);
|
|
sub.anchorAfter = copyPos(sub.anchorAfter); sub.readAfter = copyPos(sub.headAfter);
|
|
sub.copied = true;
|
|
}
|
|
if (!ok) {
|
|
array.splice(0, i + 1);
|
|
i = 0;
|
|
} else {
|
|
rebaseHistSel(sub.anchorBefore); rebaseHistSel(sub.headBefore);
|
|
rebaseHistSel(sub.anchorAfter); rebaseHistSel(sub.headAfter);
|
|
}
|
|
}
|
|
}
|
|
|
|
function rebaseHist(hist, change) {
|
|
var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1;
|
|
rebaseHistArray(hist.done, from, to, diff);
|
|
rebaseHistArray(hist.undone, from, to, diff);
|
|
}
|
|
|
|
// EVENT OPERATORS
|
|
|
|
function stopMethod() {e_stop(this);}
|
|
// Ensure an event has a stop method.
|
|
function addStop(event) {
|
|
if (!event.stop) event.stop = stopMethod;
|
|
return event;
|
|
}
|
|
|
|
function e_preventDefault(e) {
|
|
if (e.preventDefault) e.preventDefault();
|
|
else e.returnValue = false;
|
|
}
|
|
function e_stopPropagation(e) {
|
|
if (e.stopPropagation) e.stopPropagation();
|
|
else e.cancelBubble = true;
|
|
}
|
|
function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);}
|
|
CodeMirror.e_stop = e_stop;
|
|
CodeMirror.e_preventDefault = e_preventDefault;
|
|
CodeMirror.e_stopPropagation = e_stopPropagation;
|
|
|
|
function e_target(e) {return e.target || e.srcElement;}
|
|
function e_button(e) {
|
|
var b = e.which;
|
|
if (b == null) {
|
|
if (e.button & 1) b = 1;
|
|
else if (e.button & 2) b = 3;
|
|
else if (e.button & 4) b = 2;
|
|
}
|
|
if (mac && e.ctrlKey && b == 1) b = 3;
|
|
return b;
|
|
}
|
|
|
|
// EVENT HANDLING
|
|
|
|
function on(emitter, type, f) {
|
|
if (emitter.addEventListener)
|
|
emitter.addEventListener(type, f, false);
|
|
else if (emitter.attachEvent)
|
|
emitter.attachEvent("on" + type, f);
|
|
else {
|
|
var map = emitter._handlers || (emitter._handlers = {});
|
|
var arr = map[type] || (map[type] = []);
|
|
arr.push(f);
|
|
}
|
|
}
|
|
|
|
function off(emitter, type, f) {
|
|
if (emitter.removeEventListener)
|
|
emitter.removeEventListener(type, f, false);
|
|
else if (emitter.detachEvent)
|
|
emitter.detachEvent("on" + type, f);
|
|
else {
|
|
var arr = emitter._handlers && emitter._handlers[type];
|
|
if (!arr) return;
|
|
for (var i = 0; i < arr.length; ++i)
|
|
if (arr[i] == f) { arr.splice(i, 1); break; }
|
|
}
|
|
}
|
|
|
|
function signal(emitter, type /*, values...*/) {
|
|
var arr = emitter._handlers && emitter._handlers[type];
|
|
if (!arr) return;
|
|
var args = Array.prototype.slice.call(arguments, 2);
|
|
for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args);
|
|
}
|
|
|
|
var delayedCallbacks, delayedCallbackDepth = 0;
|
|
function signalLater(emitter, type /*, values...*/) {
|
|
var arr = emitter._handlers && emitter._handlers[type];
|
|
if (!arr) return;
|
|
var args = Array.prototype.slice.call(arguments, 2);
|
|
if (!delayedCallbacks) {
|
|
++delayedCallbackDepth;
|
|
delayedCallbacks = [];
|
|
setTimeout(fireDelayed, 0);
|
|
}
|
|
function bnd(f) {return function(){f.apply(null, args);};};
|
|
for (var i = 0; i < arr.length; ++i)
|
|
delayedCallbacks.push(bnd(arr[i]));
|
|
}
|
|
|
|
function fireDelayed() {
|
|
--delayedCallbackDepth;
|
|
var delayed = delayedCallbacks;
|
|
delayedCallbacks = null;
|
|
for (var i = 0; i < delayed.length; ++i) delayed[i]();
|
|
}
|
|
|
|
function hasHandler(emitter, type) {
|
|
var arr = emitter._handlers && emitter._handlers[type];
|
|
return arr && arr.length > 0;
|
|
}
|
|
|
|
CodeMirror.on = on; CodeMirror.off = off; CodeMirror.signal = signal;
|
|
|
|
// MISC UTILITIES
|
|
|
|
// Number of pixels added to scroller and sizer to hide scrollbar
|
|
var scrollerCutOff = 30;
|
|
|
|
// Returned or thrown by various protocols to signal 'I'm not
|
|
// handling this'.
|
|
var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}};
|
|
|
|
function Delayed() {this.id = null;}
|
|
Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}};
|
|
|
|
// Counts the column offset in a string, taking tabs into account.
|
|
// Used mostly to find indentation.
|
|
function countColumn(string, end, tabSize) {
|
|
if (end == null) {
|
|
end = string.search(/[^\s\u00a0]/);
|
|
if (end == -1) end = string.length;
|
|
}
|
|
for (var i = 0, n = 0; i < end; ++i) {
|
|
if (string.charAt(i) == "\t") n += tabSize - (n % tabSize);
|
|
else ++n;
|
|
}
|
|
return n;
|
|
}
|
|
CodeMirror.countColumn = countColumn;
|
|
|
|
var spaceStrs = [""];
|
|
function spaceStr(n) {
|
|
while (spaceStrs.length <= n)
|
|
spaceStrs.push(lst(spaceStrs) + " ");
|
|
return spaceStrs[n];
|
|
}
|
|
|
|
function lst(arr) { return arr[arr.length-1]; }
|
|
|
|
function selectInput(node) {
|
|
if (ios) { // Mobile Safari apparently has a bug where select() is broken.
|
|
node.selectionStart = 0;
|
|
node.selectionEnd = node.value.length;
|
|
} else node.select();
|
|
}
|
|
|
|
function indexOf(collection, elt) {
|
|
if (collection.indexOf) return collection.indexOf(elt);
|
|
for (var i = 0, e = collection.length; i < e; ++i)
|
|
if (collection[i] == elt) return i;
|
|
return -1;
|
|
}
|
|
|
|
function createObj(base, props) {
|
|
function Obj() {}
|
|
Obj.prototype = base;
|
|
var inst = new Obj();
|
|
if (props) copyObj(props, inst);
|
|
return inst;
|
|
}
|
|
|
|
function copyObj(obj, target) {
|
|
if (!target) target = {};
|
|
for (var prop in obj) if (obj.hasOwnProperty(prop)) target[prop] = obj[prop];
|
|
return target;
|
|
}
|
|
|
|
function emptyArray(size) {
|
|
for (var a = [], i = 0; i < size; ++i) a.push(undefined);
|
|
return a;
|
|
}
|
|
|
|
function bind(f) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return function(){return f.apply(null, args);};
|
|
}
|
|
|
|
var nonASCIISingleCaseWordChar = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc]/;
|
|
function isWordChar(ch) {
|
|
return /\w/.test(ch) || ch > "\x80" &&
|
|
(ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch));
|
|
}
|
|
|
|
function isEmpty(obj) {
|
|
for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false;
|
|
return true;
|
|
}
|
|
|
|
var isExtendingChar = /[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\uA66F\uA670-\uA672\uA674-\uA67D\uA69F\udc00-\udfff]/;
|
|
|
|
// DOM UTILITIES
|
|
|
|
function elt(tag, content, className, style) {
|
|
var e = document.createElement(tag);
|
|
if (className) e.className = className;
|
|
if (style) e.style.cssText = style;
|
|
if (typeof content == "string") setTextContent(e, content);
|
|
else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]);
|
|
return e;
|
|
}
|
|
|
|
function removeChildren(e) {
|
|
// IE will break all parent-child relations in subnodes when setting innerHTML
|
|
if (!ie) e.innerHTML = "";
|
|
else while (e.firstChild) e.removeChild(e.firstChild);
|
|
return e;
|
|
}
|
|
|
|
function removeChildrenAndAdd(parent, e) {
|
|
return removeChildren(parent).appendChild(e);
|
|
}
|
|
|
|
function setTextContent(e, str) {
|
|
if (ie_lt9) {
|
|
e.innerHTML = "";
|
|
e.appendChild(document.createTextNode(str));
|
|
} else e.textContent = str;
|
|
}
|
|
|
|
function getRect(node) {
|
|
return node.getBoundingClientRect();
|
|
}
|
|
CodeMirror.replaceGetRect = function(f) { getRect = f; };
|
|
|
|
// FEATURE DETECTION
|
|
|
|
// Detect drag-and-drop
|
|
var dragAndDrop = function() {
|
|
// There is *some* kind of drag-and-drop support in IE6-8, but I
|
|
// couldn't get it to work yet.
|
|
if (ie_lt9) return false;
|
|
var div = elt('div');
|
|
return "draggable" in div || "dragDrop" in div;
|
|
}();
|
|
|
|
// For a reason I have yet to figure out, some browsers disallow
|
|
// word wrapping between certain characters *only* if a new inline
|
|
// element is started between them. This makes it hard to reliably
|
|
// measure the position of things, since that requires inserting an
|
|
// extra span. This terribly fragile set of regexps matches the
|
|
// character combinations that suffer from this phenomenon on the
|
|
// various browsers.
|
|
var spanAffectsWrapping = /^$/; // Won't match any two-character string
|
|
if (gecko) spanAffectsWrapping = /$'/;
|
|
else if (safari) spanAffectsWrapping = /\-[^ \-?]|\?[^ !'\"\),.\-\/:;\?\]\}]/;
|
|
else if (chrome) spanAffectsWrapping = /\-[^ \-\.?]|\?[^ \-\.?\]\}:;!'\"\),\/]|[\.!\"#&%\)*+,:;=>\]|\}~][\(\{\[<]|\$'/;
|
|
|
|
var knownScrollbarWidth;
|
|
function scrollbarWidth(measure) {
|
|
if (knownScrollbarWidth != null) return knownScrollbarWidth;
|
|
var test = elt("div", null, null, "width: 50px; height: 50px; overflow-x: scroll");
|
|
removeChildrenAndAdd(measure, test);
|
|
if (test.offsetWidth)
|
|
knownScrollbarWidth = test.offsetHeight - test.clientHeight;
|
|
return knownScrollbarWidth || 0;
|
|
}
|
|
|
|
var zwspSupported;
|
|
function zeroWidthElement(measure) {
|
|
if (zwspSupported == null) {
|
|
var test = elt("span", "\u200b");
|
|
removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")]));
|
|
if (measure.firstChild.offsetHeight != 0)
|
|
zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !ie_lt8;
|
|
}
|
|
if (zwspSupported) return elt("span", "\u200b");
|
|
else return elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px");
|
|
}
|
|
|
|
// See if "".split is the broken IE version, if so, provide an
|
|
// alternative way to split lines.
|
|
var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) {
|
|
var pos = 0, result = [], l = string.length;
|
|
while (pos <= l) {
|
|
var nl = string.indexOf("\n", pos);
|
|
if (nl == -1) nl = string.length;
|
|
var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl);
|
|
var rt = line.indexOf("\r");
|
|
if (rt != -1) {
|
|
result.push(line.slice(0, rt));
|
|
pos += rt + 1;
|
|
} else {
|
|
result.push(line);
|
|
pos = nl + 1;
|
|
}
|
|
}
|
|
return result;
|
|
} : function(string){return string.split(/\r\n?|\n/);};
|
|
CodeMirror.splitLines = splitLines;
|
|
|
|
var hasSelection = window.getSelection ? function(te) {
|
|
try { return te.selectionStart != te.selectionEnd; }
|
|
catch(e) { return false; }
|
|
} : function(te) {
|
|
try {var range = te.ownerDocument.selection.createRange();}
|
|
catch(e) {}
|
|
if (!range || range.parentElement() != te) return false;
|
|
return range.compareEndPoints("StartToEnd", range) != 0;
|
|
};
|
|
|
|
var hasCopyEvent = (function() {
|
|
var e = elt("div");
|
|
if ("oncopy" in e) return true;
|
|
e.setAttribute("oncopy", "return;");
|
|
return typeof e.oncopy == 'function';
|
|
})();
|
|
|
|
// KEY NAMING
|
|
|
|
var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
|
|
19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End",
|
|
36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
|
|
46: "Delete", 59: ";", 91: "Mod", 92: "Mod", 93: "Mod", 109: "-", 107: "=", 127: "Delete",
|
|
186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
|
|
221: "]", 222: "'", 63276: "PageUp", 63277: "PageDown", 63275: "End", 63273: "Home",
|
|
63234: "Left", 63232: "Up", 63235: "Right", 63233: "Down", 63302: "Insert", 63272: "Delete"};
|
|
CodeMirror.keyNames = keyNames;
|
|
(function() {
|
|
// Number keys
|
|
for (var i = 0; i < 10; i++) keyNames[i + 48] = String(i);
|
|
// Alphabetic keys
|
|
for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i);
|
|
// Function keys
|
|
for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
|
|
})();
|
|
|
|
// BIDI HELPERS
|
|
|
|
function iterateBidiSections(order, from, to, f) {
|
|
if (!order) return f(from, to, "ltr");
|
|
for (var i = 0; i < order.length; ++i) {
|
|
var part = order[i];
|
|
if (part.from < to && part.to > from || from == to && part.to == from)
|
|
f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr");
|
|
}
|
|
}
|
|
|
|
function bidiLeft(part) { return part.level % 2 ? part.to : part.from; }
|
|
function bidiRight(part) { return part.level % 2 ? part.from : part.to; }
|
|
|
|
function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; }
|
|
function lineRight(line) {
|
|
var order = getOrder(line);
|
|
if (!order) return line.text.length;
|
|
return bidiRight(lst(order));
|
|
}
|
|
|
|
function lineStart(cm, lineN) {
|
|
var line = getLine(cm.doc, lineN);
|
|
var visual = visualLine(cm.doc, line);
|
|
if (visual != line) lineN = lineNo(visual);
|
|
var order = getOrder(visual);
|
|
var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual);
|
|
return Pos(lineN, ch);
|
|
}
|
|
function lineEnd(cm, lineN) {
|
|
var merged, line;
|
|
while (merged = collapsedSpanAtEnd(line = getLine(cm.doc, lineN)))
|
|
lineN = merged.find().to.line;
|
|
var order = getOrder(line);
|
|
var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line);
|
|
return Pos(lineN, ch);
|
|
}
|
|
|
|
// This is somewhat involved. It is needed in order to move
|
|
// 'visually' through bi-directional text -- i.e., pressing left
|
|
// should make the cursor go left, even when in RTL text. The
|
|
// tricky part is the 'jumps', where RTL and LTR text touch each
|
|
// other. This often requires the cursor offset to move more than
|
|
// one unit, in order to visually move one unit.
|
|
function moveVisually(line, start, dir, byUnit) {
|
|
var bidi = getOrder(line);
|
|
if (!bidi) return moveLogically(line, start, dir, byUnit);
|
|
var moveOneUnit = byUnit ? function(pos, dir) {
|
|
do pos += dir;
|
|
while (pos > 0 && isExtendingChar.test(line.text.charAt(pos)));
|
|
return pos;
|
|
} : function(pos, dir) { return pos + dir; };
|
|
var linedir = bidi[0].level;
|
|
for (var i = 0; i < bidi.length; ++i) {
|
|
var part = bidi[i], sticky = part.level % 2 == linedir;
|
|
if ((part.from < start && part.to > start) ||
|
|
(sticky && (part.from == start || part.to == start))) break;
|
|
}
|
|
var target = moveOneUnit(start, part.level % 2 ? -dir : dir);
|
|
|
|
while (target != null) {
|
|
if (part.level % 2 == linedir) {
|
|
if (target < part.from || target > part.to) {
|
|
part = bidi[i += dir];
|
|
target = part && (dir > 0 == part.level % 2 ? moveOneUnit(part.to, -1) : moveOneUnit(part.from, 1));
|
|
} else break;
|
|
} else {
|
|
if (target == bidiLeft(part)) {
|
|
part = bidi[--i];
|
|
target = part && bidiRight(part);
|
|
} else if (target == bidiRight(part)) {
|
|
part = bidi[++i];
|
|
target = part && bidiLeft(part);
|
|
} else break;
|
|
}
|
|
}
|
|
|
|
return target < 0 || target > line.text.length ? null : target;
|
|
}
|
|
|
|
function moveLogically(line, start, dir, byUnit) {
|
|
var target = start + dir;
|
|
if (byUnit) while (target > 0 && isExtendingChar.test(line.text.charAt(target))) target += dir;
|
|
return target < 0 || target > line.text.length ? null : target;
|
|
}
|
|
|
|
// Bidirectional ordering algorithm
|
|
// See http://unicode.org/reports/tr9/tr9-13.html for the algorithm
|
|
// that this (partially) implements.
|
|
|
|
// One-char codes used for character types:
|
|
// L (L): Left-to-Right
|
|
// R (R): Right-to-Left
|
|
// r (AL): Right-to-Left Arabic
|
|
// 1 (EN): European Number
|
|
// + (ES): European Number Separator
|
|
// % (ET): European Number Terminator
|
|
// n (AN): Arabic Number
|
|
// , (CS): Common Number Separator
|
|
// m (NSM): Non-Spacing Mark
|
|
// b (BN): Boundary Neutral
|
|
// s (B): Paragraph Separator
|
|
// t (S): Segment Separator
|
|
// w (WS): Whitespace
|
|
// N (ON): Other Neutrals
|
|
|
|
// Returns null if characters are ordered as they appear
|
|
// (left-to-right), or an array of sections ({from, to, level}
|
|
// objects) in the order in which they occur visually.
|
|
var bidiOrdering = (function() {
|
|
// Character types for codepoints 0 to 0xff
|
|
var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLL";
|
|
// Character types for codepoints 0x600 to 0x6ff
|
|
var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmmrrrrrrrrrrrrrrrrrr";
|
|
function charType(code) {
|
|
if (code <= 0xff) return lowTypes.charAt(code);
|
|
else if (0x590 <= code && code <= 0x5f4) return "R";
|
|
else if (0x600 <= code && code <= 0x6ff) return arabicTypes.charAt(code - 0x600);
|
|
else if (0x700 <= code && code <= 0x8ac) return "r";
|
|
else return "L";
|
|
}
|
|
|
|
var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;
|
|
var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/;
|
|
// Browsers seem to always treat the boundaries of block elements as being L.
|
|
var outerType = "L";
|
|
|
|
return function(str) {
|
|
if (!bidiRE.test(str)) return false;
|
|
var len = str.length, types = [];
|
|
for (var i = 0, type; i < len; ++i)
|
|
types.push(type = charType(str.charCodeAt(i)));
|
|
|
|
// W1. Examine each non-spacing mark (NSM) in the level run, and
|
|
// change the type of the NSM to the type of the previous
|
|
// character. If the NSM is at the start of the level run, it will
|
|
// get the type of sor.
|
|
for (var i = 0, prev = outerType; i < len; ++i) {
|
|
var type = types[i];
|
|
if (type == "m") types[i] = prev;
|
|
else prev = type;
|
|
}
|
|
|
|
// W2. Search backwards from each instance of a European number
|
|
// until the first strong type (R, L, AL, or sor) is found. If an
|
|
// AL is found, change the type of the European number to Arabic
|
|
// number.
|
|
// W3. Change all ALs to R.
|
|
for (var i = 0, cur = outerType; i < len; ++i) {
|
|
var type = types[i];
|
|
if (type == "1" && cur == "r") types[i] = "n";
|
|
else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; }
|
|
}
|
|
|
|
// W4. A single European separator between two European numbers
|
|
// changes to a European number. A single common separator between
|
|
// two numbers of the same type changes to that type.
|
|
for (var i = 1, prev = types[0]; i < len - 1; ++i) {
|
|
var type = types[i];
|
|
if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1";
|
|
else if (type == "," && prev == types[i+1] &&
|
|
(prev == "1" || prev == "n")) types[i] = prev;
|
|
prev = type;
|
|
}
|
|
|
|
// W5. A sequence of European terminators adjacent to European
|
|
// numbers changes to all European numbers.
|
|
// W6. Otherwise, separators and terminators change to Other
|
|
// Neutral.
|
|
for (var i = 0; i < len; ++i) {
|
|
var type = types[i];
|
|
if (type == ",") types[i] = "N";
|
|
else if (type == "%") {
|
|
for (var end = i + 1; end < len && types[end] == "%"; ++end) {}
|
|
var replace = (i && types[i-1] == "!") || (end < len - 1 && types[end] == "1") ? "1" : "N";
|
|
for (var j = i; j < end; ++j) types[j] = replace;
|
|
i = end - 1;
|
|
}
|
|
}
|
|
|
|
// W7. Search backwards from each instance of a European number
|
|
// until the first strong type (R, L, or sor) is found. If an L is
|
|
// found, then change the type of the European number to L.
|
|
for (var i = 0, cur = outerType; i < len; ++i) {
|
|
var type = types[i];
|
|
if (cur == "L" && type == "1") types[i] = "L";
|
|
else if (isStrong.test(type)) cur = type;
|
|
}
|
|
|
|
// N1. A sequence of neutrals takes the direction of the
|
|
// surrounding strong text if the text on both sides has the same
|
|
// direction. European and Arabic numbers act as if they were R in
|
|
// terms of their influence on neutrals. Start-of-level-run (sor)
|
|
// and end-of-level-run (eor) are used at level run boundaries.
|
|
// N2. Any remaining neutrals take the embedding direction.
|
|
for (var i = 0; i < len; ++i) {
|
|
if (isNeutral.test(types[i])) {
|
|
for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {}
|
|
var before = (i ? types[i-1] : outerType) == "L";
|
|
var after = (end < len - 1 ? types[end] : outerType) == "L";
|
|
var replace = before || after ? "L" : "R";
|
|
for (var j = i; j < end; ++j) types[j] = replace;
|
|
i = end - 1;
|
|
}
|
|
}
|
|
|
|
// Here we depart from the documented algorithm, in order to avoid
|
|
// building up an actual levels array. Since there are only three
|
|
// levels (0, 1, 2) in an implementation that doesn't take
|
|
// explicit embedding into account, we can build up the order on
|
|
// the fly, without following the level-based algorithm.
|
|
var order = [], m;
|
|
for (var i = 0; i < len;) {
|
|
if (countsAsLeft.test(types[i])) {
|
|
var start = i;
|
|
for (++i; i < len && countsAsLeft.test(types[i]); ++i) {}
|
|
order.push({from: start, to: i, level: 0});
|
|
} else {
|
|
var pos = i, at = order.length;
|
|
for (++i; i < len && types[i] != "L"; ++i) {}
|
|
for (var j = pos; j < i;) {
|
|
if (countsAsNum.test(types[j])) {
|
|
if (pos < j) order.splice(at, 0, {from: pos, to: j, level: 1});
|
|
var nstart = j;
|
|
for (++j; j < i && countsAsNum.test(types[j]); ++j) {}
|
|
order.splice(at, 0, {from: nstart, to: j, level: 2});
|
|
pos = j;
|
|
} else ++j;
|
|
}
|
|
if (pos < i) order.splice(at, 0, {from: pos, to: i, level: 1});
|
|
}
|
|
}
|
|
if (order[0].level == 1 && (m = str.match(/^\s+/))) {
|
|
order[0].from = m[0].length;
|
|
order.unshift({from: 0, to: m[0].length, level: 0});
|
|
}
|
|
if (lst(order).level == 1 && (m = str.match(/\s+$/))) {
|
|
lst(order).to -= m[0].length;
|
|
order.push({from: len - m[0].length, to: len, level: 0});
|
|
}
|
|
if (order[0].level != lst(order).level)
|
|
order.push({from: len, to: len, level: order[0].level});
|
|
|
|
return order;
|
|
};
|
|
})();
|
|
|
|
// THE END
|
|
|
|
CodeMirror.version = "3.1";
|
|
|
|
return CodeMirror;
|
|
})();
|