editor-document
Version:
applyDelta, Delta, Document, Position, Range
1,418 lines (1,387 loc) • 48.3 kB
JavaScript
'use strict';
function applyDelta(docLines, delta) {
const startRow = delta.start.row;
const startColumn = delta.start.column;
const line = docLines[startRow] || "";
switch (delta.action) {
case "insert": {
const lines = delta.lines;
if (lines.length === 1) {
docLines[startRow] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn);
}
else {
// The following three lines are replaced by the one that follows because eslint prefer-spread.
// let args: unknown[] = [startRow, 1];
// args = args.concat(delta.lines);
// docLines.splice.apply(docLines, args);
docLines.splice(startRow, 1, ...delta.lines);
docLines[startRow] = line.substring(0, startColumn) + docLines[startRow];
docLines[startRow + delta.lines.length - 1] += line.substring(startColumn);
}
break;
}
case "remove": {
const endColumn = delta.end.column;
const endRow = delta.end.row;
if (startRow === endRow) {
docLines[startRow] = line.substring(0, startColumn) + line.substring(endColumn);
}
else {
docLines.splice(startRow, endRow - startRow + 1, line.substring(0, startColumn) + docLines[endRow].substring(endColumn));
}
break;
}
}
}
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
function __values(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
}
function __read(o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
}
function __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
function isFunction(value) {
return typeof value === 'function';
}
function createErrorClass(createImpl) {
var _super = function (instance) {
Error.call(instance);
instance.stack = new Error().stack;
};
var ctorFunc = createImpl(_super);
ctorFunc.prototype = Object.create(Error.prototype);
ctorFunc.prototype.constructor = ctorFunc;
return ctorFunc;
}
var UnsubscriptionError = createErrorClass(function (_super) {
return function UnsubscriptionErrorImpl(errors) {
_super(this);
this.message = errors
? errors.length + " errors occurred during unsubscription:\n" + errors.map(function (err, i) { return i + 1 + ") " + err.toString(); }).join('\n ')
: '';
this.name = 'UnsubscriptionError';
this.errors = errors;
};
});
function arrRemove(arr, item) {
if (arr) {
var index = arr.indexOf(item);
0 <= index && arr.splice(index, 1);
}
}
var Subscription = (function () {
function Subscription(initialTeardown) {
this.initialTeardown = initialTeardown;
this.closed = false;
this._parentage = null;
this._finalizers = null;
}
Subscription.prototype.unsubscribe = function () {
var e_1, _a, e_2, _b;
var errors;
if (!this.closed) {
this.closed = true;
var _parentage = this._parentage;
if (_parentage) {
this._parentage = null;
if (Array.isArray(_parentage)) {
try {
for (var _parentage_1 = __values(_parentage), _parentage_1_1 = _parentage_1.next(); !_parentage_1_1.done; _parentage_1_1 = _parentage_1.next()) {
var parent_1 = _parentage_1_1.value;
parent_1.remove(this);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_parentage_1_1 && !_parentage_1_1.done && (_a = _parentage_1.return)) _a.call(_parentage_1);
}
finally { if (e_1) throw e_1.error; }
}
}
else {
_parentage.remove(this);
}
}
var initialFinalizer = this.initialTeardown;
if (isFunction(initialFinalizer)) {
try {
initialFinalizer();
}
catch (e) {
errors = e instanceof UnsubscriptionError ? e.errors : [e];
}
}
var _finalizers = this._finalizers;
if (_finalizers) {
this._finalizers = null;
try {
for (var _finalizers_1 = __values(_finalizers), _finalizers_1_1 = _finalizers_1.next(); !_finalizers_1_1.done; _finalizers_1_1 = _finalizers_1.next()) {
var finalizer = _finalizers_1_1.value;
try {
execFinalizer(finalizer);
}
catch (err) {
errors = errors !== null && errors !== void 0 ? errors : [];
if (err instanceof UnsubscriptionError) {
errors = __spreadArray(__spreadArray([], __read(errors)), __read(err.errors));
}
else {
errors.push(err);
}
}
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (_finalizers_1_1 && !_finalizers_1_1.done && (_b = _finalizers_1.return)) _b.call(_finalizers_1);
}
finally { if (e_2) throw e_2.error; }
}
}
if (errors) {
throw new UnsubscriptionError(errors);
}
}
};
Subscription.prototype.add = function (teardown) {
var _a;
if (teardown && teardown !== this) {
if (this.closed) {
execFinalizer(teardown);
}
else {
if (teardown instanceof Subscription) {
if (teardown.closed || teardown._hasParent(this)) {
return;
}
teardown._addParent(this);
}
(this._finalizers = (_a = this._finalizers) !== null && _a !== void 0 ? _a : []).push(teardown);
}
}
};
Subscription.prototype._hasParent = function (parent) {
var _parentage = this._parentage;
return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));
};
Subscription.prototype._addParent = function (parent) {
var _parentage = this._parentage;
this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;
};
Subscription.prototype._removeParent = function (parent) {
var _parentage = this._parentage;
if (_parentage === parent) {
this._parentage = null;
}
else if (Array.isArray(_parentage)) {
arrRemove(_parentage, parent);
}
};
Subscription.prototype.remove = function (teardown) {
var _finalizers = this._finalizers;
_finalizers && arrRemove(_finalizers, teardown);
if (teardown instanceof Subscription) {
teardown._removeParent(this);
}
};
Subscription.EMPTY = (function () {
var empty = new Subscription();
empty.closed = true;
return empty;
})();
return Subscription;
}());
Subscription.EMPTY;
function isSubscription(value) {
return (value instanceof Subscription ||
(value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe)));
}
function execFinalizer(finalizer) {
if (isFunction(finalizer)) {
finalizer();
}
else {
finalizer.unsubscribe();
}
}
var config = {
onUnhandledError: null,
onStoppedNotification: null,
Promise: undefined,
useDeprecatedSynchronousErrorHandling: false,
useDeprecatedNextContext: false,
};
var timeoutProvider = {
setTimeout: function (handler, timeout) {
var args = [];
for (var _i = 2; _i < arguments.length; _i++) {
args[_i - 2] = arguments[_i];
}
return setTimeout.apply(void 0, __spreadArray([handler, timeout], __read(args)));
},
clearTimeout: function (handle) {
var delegate = timeoutProvider.delegate;
return ((delegate === null || delegate === void 0 ? void 0 : delegate.clearTimeout) || clearTimeout)(handle);
},
delegate: undefined,
};
function reportUnhandledError(err) {
timeoutProvider.setTimeout(function () {
{
throw err;
}
});
}
function noop() { }
function errorContext(cb) {
{
cb();
}
}
var Subscriber = (function (_super) {
__extends(Subscriber, _super);
function Subscriber(destination) {
var _this = _super.call(this) || this;
_this.isStopped = false;
if (destination) {
_this.destination = destination;
if (isSubscription(destination)) {
destination.add(_this);
}
}
else {
_this.destination = EMPTY_OBSERVER;
}
return _this;
}
Subscriber.create = function (next, error, complete) {
return new SafeSubscriber(next, error, complete);
};
Subscriber.prototype.next = function (value) {
if (this.isStopped) ;
else {
this._next(value);
}
};
Subscriber.prototype.error = function (err) {
if (this.isStopped) ;
else {
this.isStopped = true;
this._error(err);
}
};
Subscriber.prototype.complete = function () {
if (this.isStopped) ;
else {
this.isStopped = true;
this._complete();
}
};
Subscriber.prototype.unsubscribe = function () {
if (!this.closed) {
this.isStopped = true;
_super.prototype.unsubscribe.call(this);
this.destination = null;
}
};
Subscriber.prototype._next = function (value) {
this.destination.next(value);
};
Subscriber.prototype._error = function (err) {
try {
this.destination.error(err);
}
finally {
this.unsubscribe();
}
};
Subscriber.prototype._complete = function () {
try {
this.destination.complete();
}
finally {
this.unsubscribe();
}
};
return Subscriber;
}(Subscription));
var _bind = Function.prototype.bind;
function bind(fn, thisArg) {
return _bind.call(fn, thisArg);
}
var ConsumerObserver = (function () {
function ConsumerObserver(partialObserver) {
this.partialObserver = partialObserver;
}
ConsumerObserver.prototype.next = function (value) {
var partialObserver = this.partialObserver;
if (partialObserver.next) {
try {
partialObserver.next(value);
}
catch (error) {
handleUnhandledError(error);
}
}
};
ConsumerObserver.prototype.error = function (err) {
var partialObserver = this.partialObserver;
if (partialObserver.error) {
try {
partialObserver.error(err);
}
catch (error) {
handleUnhandledError(error);
}
}
else {
handleUnhandledError(err);
}
};
ConsumerObserver.prototype.complete = function () {
var partialObserver = this.partialObserver;
if (partialObserver.complete) {
try {
partialObserver.complete();
}
catch (error) {
handleUnhandledError(error);
}
}
};
return ConsumerObserver;
}());
var SafeSubscriber = (function (_super) {
__extends(SafeSubscriber, _super);
function SafeSubscriber(observerOrNext, error, complete) {
var _this = _super.call(this) || this;
var partialObserver;
if (isFunction(observerOrNext) || !observerOrNext) {
partialObserver = {
next: (observerOrNext !== null && observerOrNext !== void 0 ? observerOrNext : undefined),
error: error !== null && error !== void 0 ? error : undefined,
complete: complete !== null && complete !== void 0 ? complete : undefined,
};
}
else {
var context_1;
if (_this && config.useDeprecatedNextContext) {
context_1 = Object.create(observerOrNext);
context_1.unsubscribe = function () { return _this.unsubscribe(); };
partialObserver = {
next: observerOrNext.next && bind(observerOrNext.next, context_1),
error: observerOrNext.error && bind(observerOrNext.error, context_1),
complete: observerOrNext.complete && bind(observerOrNext.complete, context_1),
};
}
else {
partialObserver = observerOrNext;
}
}
_this.destination = new ConsumerObserver(partialObserver);
return _this;
}
return SafeSubscriber;
}(Subscriber));
function handleUnhandledError(error) {
{
reportUnhandledError(error);
}
}
function defaultErrorHandler(err) {
throw err;
}
var EMPTY_OBSERVER = {
closed: true,
next: noop,
error: defaultErrorHandler,
complete: noop,
};
var observable = (function () { return (typeof Symbol === 'function' && Symbol.observable) || '@@observable'; })();
function identity(x) {
return x;
}
function pipeFromArray(fns) {
if (fns.length === 0) {
return identity;
}
if (fns.length === 1) {
return fns[0];
}
return function piped(input) {
return fns.reduce(function (prev, fn) { return fn(prev); }, input);
};
}
var Observable = (function () {
function Observable(subscribe) {
if (subscribe) {
this._subscribe = subscribe;
}
}
Observable.prototype.lift = function (operator) {
var observable = new Observable();
observable.source = this;
observable.operator = operator;
return observable;
};
Observable.prototype.subscribe = function (observerOrNext, error, complete) {
var _this = this;
var subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);
errorContext(function () {
var _a = _this, operator = _a.operator, source = _a.source;
subscriber.add(operator
?
operator.call(subscriber, source)
: source
?
_this._subscribe(subscriber)
:
_this._trySubscribe(subscriber));
});
return subscriber;
};
Observable.prototype._trySubscribe = function (sink) {
try {
return this._subscribe(sink);
}
catch (err) {
sink.error(err);
}
};
Observable.prototype.forEach = function (next, promiseCtor) {
var _this = this;
promiseCtor = getPromiseCtor(promiseCtor);
return new promiseCtor(function (resolve, reject) {
var subscriber = new SafeSubscriber({
next: function (value) {
try {
next(value);
}
catch (err) {
reject(err);
subscriber.unsubscribe();
}
},
error: reject,
complete: resolve,
});
_this.subscribe(subscriber);
});
};
Observable.prototype._subscribe = function (subscriber) {
var _a;
return (_a = this.source) === null || _a === void 0 ? void 0 : _a.subscribe(subscriber);
};
Observable.prototype[observable] = function () {
return this;
};
Observable.prototype.pipe = function () {
var operations = [];
for (var _i = 0; _i < arguments.length; _i++) {
operations[_i] = arguments[_i];
}
return pipeFromArray(operations)(this);
};
Observable.prototype.toPromise = function (promiseCtor) {
var _this = this;
promiseCtor = getPromiseCtor(promiseCtor);
return new promiseCtor(function (resolve, reject) {
var value;
_this.subscribe(function (x) { return (value = x); }, function (err) { return reject(err); }, function () { return resolve(value); });
});
};
Observable.create = function (subscribe) {
return new Observable(subscribe);
};
return Observable;
}());
function getPromiseCtor(promiseCtor) {
var _a;
return (_a = promiseCtor !== null && promiseCtor !== void 0 ? promiseCtor : config.Promise) !== null && _a !== void 0 ? _a : Promise;
}
function isObserver(value) {
return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);
}
function isSubscriber(value) {
return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));
}
// import { refChange } from '@stemcstudio/common';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function refChange(uuid, moniker, chnage) {
}
/**
* (session)
*/
class EventBus {
/**
* Used for monitoring subscriptions.
*/
#uuid = `${Math.random()}`;
#moniker;
#monitor;
/**
* Each event name may have multiple callback functions.
*/
#subscribers;
/**
* In general, you don't want to supply the $monitor argument, letting it default to true.
*
* @param moniker A name that can be used to recognize this event bus.
* @param monitor Determines whether reference counting due to listeners will be performed.
* @param warning Determines whether a warning will be logged to the console.
*/
constructor(moniker, monitor, warning = monitor) {
this.#moniker = moniker;
this.#monitor = monitor;
if (warning && !monitor) {
// eslint-disable-next-line no-console
console.warn(`${moniker} has elected to bypass monitoring. It may be leaking subscriptions?`);
}
}
/**
* For testing purposes, we want to know whether unsubscribing is cleaning up the listeners.
* @param eventName The name of the event.
*/
subscriberCountByEvent(eventName) {
if (this.#subscribers) {
return this.#subscribers[eventName].length;
}
else {
return 0;
}
}
/**
* Synchronous dispatch of the event to subscribers.
*/
send(eventName, event) {
return this.dispatch(eventName, event);
}
/**
* Asynchronous dispatch of the event to subscribers.
*
* @param eventName The name of the event.
* @param event The value of the event.
* @param timeout The delay, in milliseconds, for emitting the event.
* @returns A promise that resolves when the event has been dispatched.
*/
post(eventName, event, timeout = 0) {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
try {
const count = this.dispatch(eventName, event);
try {
resolve(count);
}
catch (err) {
// Ignore.
// Perhaps there should be a callback parameter that allows this case to be handled?
}
}
catch (err) {
reject(err);
}
}, timeout);
});
}
/**
* Creates an Observable for one event type.
* @param eventName The name of the event that the observable is being created for.
*/
events(eventName) {
return new Observable((observer) => {
// TODO: Interesting that the source is not used.
// Does that mean that the source will not be present to the subscriber?
function next(value) {
observer.next(value);
}
// By returning the function that removes the event listener,
// unsubscribe() on the subscription to the Observable cleans up the subscribers.
return this.addEventListener(eventName, next);
});
}
/**
* @deprecated Use an observable
*/
on(eventName, callback) {
return this.addEventListener(eventName, callback);
}
/**
* @deprecated Use an Observable.
*/
off(eventName, callback) {
return this.removeEventListener(eventName, callback);
}
addEventListener(eventName, callback) {
if (this.#monitor) {
refChange(this.#uuid, this.#moniker);
}
this.#subscribers = this.#subscribers || {};
let listeners = this.#subscribers[eventName];
if (!listeners) {
listeners = this.#subscribers[eventName] = [];
}
if (listeners.indexOf(callback) === -1) {
listeners.push(callback);
}
return () => {
this.removeEventListener(eventName, callback);
};
}
removeEventListener(eventName, callback) {
if (this.#monitor) {
refChange(this.#uuid, this.#moniker);
}
this.#subscribers = this.#subscribers || {};
const listeners = this.#subscribers[eventName];
if (!listeners) {
return;
}
const index = listeners.indexOf(callback);
if (index !== -1) {
listeners.splice(index, 1);
}
}
/**
* Calls each listener subscribed to the eventName passing the event and the source.
* Returns the number of listeners who received the event.
*/
dispatch(eventName, event) {
/**
* The listeners subscribed to the specified event name
*/
let listeners = (this.#subscribers || {})[eventName];
if (!listeners) {
return 0;
}
// slice just makes a copy so that we don't mess up on array bounds.
// It's a bit expensive though?
listeners = listeners.slice();
let count = 0;
for (const listener of listeners) {
listener(event);
count += 1;
}
return count;
}
}
/**
* Constructs a position object from a zero-based row and zero-based column.
* @param row The zero-based row.
* @param column The zero-based column.
*/
function position(row, column) {
return { row, column };
}
/**
* Returns 0 if positions are equal, +1 if p1 comes after p2, -1 if p1 comes before p2.
*/
function comparePositions(p1, p2) {
if (p1.row > p2.row) {
return 1;
}
else if (p1.row < p2.row) {
return -1;
}
else {
if (p1.column > p2.column) {
return 1;
}
else if (p1.column < p2.column) {
return -1;
}
else {
return 0;
}
}
}
/**
* Determines whether positions p1 and p2 are equal.
* @param p1 The first position.
* @param p2 The second position.
*/
function equalPositions(p1, p2) {
return p1.row === p2.row && p1.column === p2.column;
}
function range(start, end) {
return { start, end };
}
/**
* The range is empty if the start and end position coincide.
*/
function isEmptyRange(range) {
return equalPositions(range.start, range.end);
}
/**
* Copies a Position.
*/
function clonePos(pos) {
return { row: pos.row, column: pos.column };
}
/**
* Constructs a Position from row and column.
*/
function pos(row, column) {
return { row: row, column: column };
}
const $split = (function () {
function foo(text) {
return text.replace(/\r\n|\r/g, "\n").split("\n");
}
function bar(text) {
return text.split(/\r\n|\r|\n/);
}
// Determine whether the split function performs as we expect.
// Here we attempt to separate a string of three separators.
// If all works out, we should get back an array of four (4) empty strings.
if ("aaa".split(/a/).length === 0) {
return foo;
}
else {
// In Chrome, this is the mainline because the result
// of the test condition length is 4.
return bar;
}
})();
/*
function clipPosition(doc: Document, position: Position): Position {
const length = doc.getLength();
if (position.row >= length) {
position.row = Math.max(0, length - 1);
position.column = doc.getLine(length - 1).length;
}
else {
position.row = Math.max(0, position.row);
position.column = Math.min(Math.max(position.column, 0), doc.getLine(position.row).length);
}
return position;
}
*/
const CHANGE = 'change';
const CHANGE_NEW_LINE_MODE = 'changeNewLineMode';
/**
*
*/
class Document {
/**
* The lines of text.
* These lines do not include a line terminating character.
*/
#lines = [];
/**
*
*/
#autoNewLine = "";
/**
*
*/
#newLineMode = "auto";
/**
* A source of 'change' events that is observable.
*/
/*
public readonly changeEvents: Observable<Delta>;
*/
/**
*
*/
#change = new EventBus('', true);
change$ = this.#change.events(CHANGE);
#changeNewLineMode = new EventBus('', true);
changeNewLineMode$ = this.#changeNewLineMode.events(CHANGE_NEW_LINE_MODE);
/**
* Maintains a count of the number of references to this instance of Document.
*/
#refCount = 1;
/**
* If text is included, the Document contains those strings; otherwise, it's empty.
* A `change` event will be emitted. But does anyone see it?
*
* @param textOrLines
*/
constructor(textOrLines) {
this.#lines = [""];
/*
this.changeEvents = new Observable<Delta>((observer: Observer<Delta>) => {
function changeListener(value: Delta, source: Document) {
observer.next(value);
}
this.addChangeListener(changeListener);
return () => {
this.removeChangeListener(changeListener);
};
});
*/
// There has to be one line at least in the document. If you pass an empty
// string to the insert function, nothing will happen. Workaround.
if (textOrLines.length === 0) {
this.#lines = [""];
}
else if (Array.isArray(textOrLines)) {
this.insertMergedLines({ row: 0, column: 0 }, textOrLines);
}
else {
this.insert({ row: 0, column: 0 }, textOrLines);
}
}
#destructor() {
this.#lines.length = 0;
}
addRef() {
this.#refCount++;
return this.#refCount;
}
release() {
this.#refCount--;
if (this.#refCount === 0) {
this.#destructor();
}
else if (this.#refCount < 0) {
throw new Error("Document refCount is negative.");
}
return this.#refCount;
}
/**
* Replaces all the lines in the current `Document` with the value of `text`.
* A `change` event will be emitted.
*/
setValue(text) {
const row = this.getLength() - 1;
const start = position(0, 0);
const end = position(row, this.getLine(row).length);
// FIXME: Can we avoid the temporary objects?
this.remove(range(start, end));
this.insert({ row: 0, column: 0 }, text);
}
/**
* Returns all the lines in the document as a single string, joined by the new line character.
*/
getValue() {
return this.#lines.join(this.getNewLineCharacter());
}
/**
* Determines the newline character that is present in the presented text
* and caches the result in $autoNewLine.
* Emits 'changeNewLineMode'.
*/
#detect_new_line(text) {
const match = text.match(/^.*?(\r\n|\r|\n)/m);
this.#autoNewLine = match ? match[1] : "\n";
this.#changeNewLineMode.send(CHANGE_NEW_LINE_MODE, this.#autoNewLine);
}
/**
* Returns the newline character that's being used, depending on the value of `newLineMode`.
* If `newLineMode == windows`, `\r\n` is returned.
* If `newLineMode == unix`, `\n` is returned.
* If `newLineMode == auto`, the value of `autoNewLine` is returned.
*/
getNewLineCharacter() {
switch (this.#newLineMode) {
case "windows":
return "\r\n";
case "unix":
return "\n";
default:
return this.#autoNewLine || "\n";
}
}
/**
* Sets the new line mode.
*
* newLineMode is the newline mode to use; can be either `windows`, `unix`, or `auto`.
* Emits 'changeNewLineMode'
*/
setNewLineMode(newLineMode) {
if (this.#newLineMode === newLineMode) {
return;
}
this.#newLineMode = newLineMode;
this.#changeNewLineMode.send(CHANGE_NEW_LINE_MODE, this.#newLineMode);
}
/**
* Returns the type of newlines being used; either `windows`, `unix`, or `auto`.
*/
getNewLineMode() {
return this.#newLineMode;
}
/**
* Returns `true` if `text` is a newline character (either `\r\n`, `\r`, or `\n`).
*
* @param text The text to check.
*/
isNewLine(text) {
return (text === "\r\n" || text === "\r" || text === "\n");
}
/**
* Returns a verbatim copy of the given line as it is in the document.
*
* @param row The row index to retrieve.
*/
getLine(row) {
return this.#lines[row] || "";
}
/**
* Returns a COPY of the lines between and including `firstRow` and `lastRow`.
* These lines do not include the line terminator.
*
* @param firstRow The first row index to retrieve.
* @param lastRow The final row index to retrieve.
*/
getLines(firstRow, lastRow) {
// The semantics of slice are that it does not include the end index.
const end = lastRow + 1;
return this.#lines.slice(firstRow, end);
}
/**
* Returns a COPY of the lines in the document.
* These lines do not include the line terminator.
*/
getAllLines() {
return this.#lines.slice(0, this.#lines.length);
}
/**
* Returns the number of rows in the document.
*/
getLength() {
return this.#lines.length;
}
/**
* Returns all the text corresponding to the range with line terminators.
*/
getTextRange(range) {
return this.getLinesForRange(range).join(this.getNewLineCharacter());
}
/**
* Returns all the text within `range` as an array of lines.
*/
getLinesForRange(range) {
let lines;
if (range.start.row === range.end.row) {
// Handle a single-line range.
lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)];
}
else {
// Handle a multi-line range.
lines = this.getLines(range.start.row, range.end.row);
lines[0] = (lines[0] || "").substring(range.start.column);
const l = lines.length - 1;
if (range.end.row - range.start.row === l) {
lines[l] = lines[l].substring(0, range.end.column);
}
}
return lines;
}
/**
* Inserts a block of `text` at the indicated `position`.
* Returns the end position of the inserted text, the character immediately after the last character inserted.
* This method also triggers the 'change' event.
*/
insert(position, text) {
// Only detect new lines if the document has no line break yet.
if (this.getLength() <= 1) {
this.#detect_new_line(text);
}
return this.insertMergedLines(position, $split(text));
}
/**
* Inserts `text` into the `position` at the current row. This method also triggers the `"change"` event.
*
* This differs from the `insert` method in two ways:
* 1. This does NOT handle newline characters (single-line text only).
* 2. This is faster than the `insert` method for single-line text insertions.
*/
insertInLine(position, text) {
const start = this.clippedPos(position.row, position.column);
const end = pos(position.row, position.column + text.length);
this.applyDelta({
start: start,
end: end,
action: "insert",
lines: [text]
});
return clonePos(end);
}
/**
* Clips the position so that it refers to the nearest valid position.
*/
clippedPos(row, column) {
const length = this.getLength();
let rowTooBig = false;
if (row === void 0) {
row = length;
}
else if (row < 0) {
row = 0;
}
else if (row >= length) {
row = length - 1;
rowTooBig = true;
}
const line = this.getLine(row);
if (rowTooBig) {
column = line.length;
}
column = Math.min(Math.max(column, 0), line.length);
return { row: row, column: column };
}
/**
* Inserts the elements in `lines` into the document as full lines (does not merge with existing line), starting at the row index given by `row`.
* This method also triggers the `"change"` event.
*/
insertFullLines(row, lines) {
// Clip to document.
// Allow one past the document end.
row = Math.min(Math.max(row, 0), this.getLength());
// Calculate insertion point.
let column = 0;
if (row < this.getLength()) {
// Insert before the specified row.
lines = lines.concat([""]);
column = 0;
}
else {
// Insert after the last row in the document.
lines = [""].concat(lines);
row--;
column = this.#lines[row].length;
}
// Insert.
return this.insertMergedLines({ row: row, column: column }, lines);
}
/**
* Inserts the text in `lines` into the document, starting at the `position` given.
* Returns the end position of the inserted text.
* This method also triggers the 'change' event.
*/
insertMergedLines(position, lines) {
const start = this.clippedPos(position.row, position.column);
const end = {
row: start.row + lines.length - 1,
column: (lines.length === 1 ? start.column : 0) + lines[lines.length - 1].length
};
this.applyDelta({
start: start,
end: end,
action: "insert",
lines: lines
});
return clonePos(end);
}
/**
* Removes the `range` from the document.
* This method triggers a 'change' event.
*
* @param range A specified Range to remove
* @return Returns the new `start` property of the range.
* If `range` is empty, this function returns the unmodified value of `range.start`.
*/
remove(range) {
const start = this.clippedPos(range.start.row, range.start.column);
const end = this.clippedPos(range.end.row, range.end.column);
this.applyDelta({
start: start,
end: end,
action: "remove",
lines: this.getLinesForRange({ start: start, end: end })
});
return clonePos(start);
}
/**
* Removes the specified columns from the `row`.
* This method also triggers the `'change'` event.
*
* @param row The row to remove from.
* @param startColumn The column to start removing at.
* @param endColumn The column to stop removing at.
* @returns An object containing `startRow` and `startColumn`, indicating the new row and column values.<br/>If `startColumn` is equal to `endColumn`, this function returns nothing.
*/
removeInLine(row, startColumn, endColumn) {
const start = this.clippedPos(row, startColumn);
const end = this.clippedPos(row, endColumn);
this.applyDelta({
start: start,
end: end,
action: "remove",
lines: this.getLinesForRange({ start: start, end: end })
});
return clonePos(start);
}
/**
* Removes a range of full lines and returns a COPY of the removed lines.
* This method also triggers the `"change"` event.
*
* @param firstRow The first row to be removed
* @param lastRow The last row to be removed
*/
removeFullLines(firstRow, lastRow) {
// Clip to document.
firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1);
lastRow = Math.min(Math.max(0, lastRow), this.getLength() - 1);
// Calculate deletion range.
// Delete the ending new line unless we're at the end of the document.
// If we're at the end of the document, delete the starting new line.
const deleteFirstNewLine = lastRow === this.getLength() - 1 && firstRow > 0;
const deleteLastNewLine = lastRow < this.getLength() - 1;
const startRow = (deleteFirstNewLine ? firstRow - 1 : firstRow);
const startCol = (deleteFirstNewLine ? this.getLine(startRow).length : 0);
const endRow = (deleteLastNewLine ? lastRow + 1 : lastRow);
const endCol = (deleteLastNewLine ? 0 : this.getLine(endRow).length);
const start = position(startRow, startCol);
const end = position(endRow, endCol);
/**
* A copy of delelted lines with line terminators omitted (maintains previous behavior).
*/
const deletedLines = this.getLines(firstRow, lastRow);
this.applyDelta({
start,
end,
action: "remove",
lines: this.getLinesForRange(range(start, end))
});
return deletedLines;
}
/**
* Removes the new line between `row` and the row immediately following it.
*
* @param row The row to check.
*/
removeNewLine(row) {
if (row < this.getLength() - 1 && row >= 0) {
this.applyDelta({
start: pos(row, this.getLine(row).length),
end: pos(row + 1, 0),
action: "remove",
lines: ["", ""]
});
}
}
/**
* Replaces a range in the document with the new `text`.
* Returns the end position of the change.
* This method triggers a 'change' event for the removal.
* This method triggers a 'change' event for the insertion.
*/
replace(range, newText) {
if (newText.length === 0 && isEmptyRange(range)) {
// If the range is empty then the range.start and range.end will be the same.
return range.end;
}
const oldText = this.getTextRange(range);
// Shortcut: If the text we want to insert is the same as it is already
// in the document, we don't have to replace anything.
if (newText === oldText) {
return range.end;
}
this.remove(range);
return this.insert(range.start, newText);
}
/**
* Applies all the changes previously accumulated.
*/
applyDeltas(deltas) {
for (let i = 0; i < deltas.length; i++) {
this.applyDelta(deltas[i]);
}
}
/**
* Reverts any changes previously applied.
*/
revertDeltas(deltas) {
for (let i = deltas.length - 1; i >= 0; i--) {
this.revertDelta(deltas[i]);
}
}
/**
* Applies `delta` (insert and remove actions) to the document and triggers the 'change' event.
*/
applyDelta(delta) {
const isInsert = delta.action === "insert";
// An empty range is a NOOP.
if (isInsert ? delta.lines.length <= 1 && !delta.lines[0] : equalPositions(delta.start, delta.end)) {
return;
}
if (isInsert && delta.lines.length > 20000) {
this.#split_and_apply_large_change(delta, 20000);
}
applyDelta(this.#lines, delta);
this.#change.send(CHANGE, delta);
}
#split_and_apply_large_change(delta, MAX) {
// Split large insert deltas. This is necessary because:
// 1. We need to support splicing delta lines into the document via $lines.splice.apply(...)
// 2. fn.apply() doesn't work for a large number of params. The smallest threshold is on chrome 40 ~42000.
// we use 20000 to leave some space for actual stack
//
// To Do: Ideally we'd be consistent and also split 'delete' deltas. We don't do this now, because delete
// delta handling is too slow. If we make delete delta handling faster we can split all large deltas
// as shown in https://gist.github.com/aldendaniels/8367109#file-document-snippet-js
// If we do this, update validateDelta() to limit the number of lines in a delete delta.
const lines = delta.lines;
const l = lines.length;
const row = delta.start.row;
let column = delta.start.column;
let from = 0;
let to = 0;
let keepGoing = true;
do {
from = to;
to += MAX - 1;
const chunk = lines.slice(from, to);
if (to > l) {
// Update remaining delta.
delta.lines = chunk;
delta.start.row = row + from;
delta.start.column = column;
keepGoing = false;
break;
}
if (keepGoing) {
chunk.push("");
this.applyDelta({
start: pos(row + from, column),
end: pos(row + to, column = 0),
action: delta.action,
lines: chunk
});
}
} while (keepGoing);
}
/**
* Reverts `delta` from the document.
* A delta object (can include "insert" and "remove" actions)
*/
revertDelta(delta) {
this.applyDelta({
start: clonePos(delta.start),
end: clonePos(delta.end),
action: (delta.action === "insert" ? "remove" : "insert"),
lines: delta.lines.slice()
});
}
/**
* Converts an index position in a document to a `{row, column}` object.
*
* Index refers to the "absolute position" of a character in the document. For example:
*
* ```javascript
* x = 0; // 10 characters, plus one for newline
* y = -1;
* ```
*
* Here, `y` is an index 15: 11 characters for the first row, and 5 characters until `y` in the second.
*
* @param index An index to convert
* @param startRow The row from which to start the conversion
* @returns An object of the `index` position.
*/
indexToPosition(index, startRow = 0) {
/**
* A local reference to improve performance in the loop.
*/
const lines = this.#lines;
const newlineLength = this.getNewLineCharacter().length;
const l = lines.length;
for (let i = startRow || 0; i < l; i++) {
index -= lines[i].length + newlineLength;
if (index < 0)
return { row: i, column: index + lines[i].length + newlineLength };
}
return { row: l - 1, column: lines[l - 1].length };
}
/**
* Converts the `position` in a document to the character's zero-based index.
*
* Index refers to the "absolute position" of a character in the document. For example:
*
* ```javascript
* x = 0; // 10 characters, plus one for newline
* y = -1;
* ```
*
* Here, `y` is an index 15: 11 characters for the first row, and 5 characters until `y` in the second.
*
* @param position The `{row, column}` to convert.
* @param startRow The row from which to start the conversion. Defaults to zero.
*/
positionToIndex(position, startRow = 0) {
/**
* A local reference to improve performance in the loop.
*/
const lines = this.#lines;
const newlineLength = this.getNewLineCharacter().length;
let index = 0;
const row = Math.min(position.row, lines.length);
for (let i = startRow || 0; i < row; ++i) {
index += lines[i].length + newlineLength;
}
return index + position.column;
}
}
exports.Document = Document;
exports.applyDelta = applyDelta;
exports.comparePositions = comparePositions;
exports.equalPositions = equalPositions;
exports.isEmptyRange = isEmptyRange;
exports.position = position;
exports.range = range;
//# sourceMappingURL=index.js.map