Ramesh-vani's picture
Upload 32 files
341f1e9 verified
/*
* Copyright (c) 2019 Convergence Labs, Inc.
*
* This file is part of the CodeMirror Collaborative Extensions, which is
* released under the terms of the MIT license. A copy of the MIT license
* is usually provided as part of this source code package in the LICENCE
* file. If it was not, please see <https://opensource.org/licenses/MIT>
*/
import {Editor, EditorChangeCancellable, EditorChangeLinkedList} from "codemirror";
import {IEditorContentManagerOptions} from "./IEditorContentManagerOptions";
import {Validation} from "./Validation";
/**
* The EditorContentManager facilitates listening to local content changes and
* the playback of remote content changes into the editor.
*/
export class EditorContentManager {
/**
* Option defaults.
*
* @internal
*/
private static readonly _DEFAULTS = {
onInsert: () => {
// no-op
},
onReplace: () => {
// no-op
},
onDelete: () => {
// no-op
},
remoteOrigin: "remote"
};
/**
* The options that configure the EditorContentManager.
* @internal
*/
private readonly _options: IEditorContentManagerOptions;
/**
* A flag denoting if outgoing events should be suppressed.
* @internal
*/
private _suppress: boolean;
private _operationQueue: IOperation[];
/**
* Constructs a new EditorContentManager using the supplied options.
*
* @param options
* The options that configure the EditorContentManager.
*/
constructor(options: IEditorContentManagerOptions) {
this._options = {...EditorContentManager._DEFAULTS, ...options};
Validation.assertDefined(this._options, "options");
Validation.assertDefined(this._options.editor, "options.editor");
Validation.assertFunction(this._options.onInsert, "options.onInsert");
Validation.assertFunction(this._options.onReplace, "options.onReplace");
Validation.assertFunction(this._options.onDelete, "options.onDelete");
this._options.editor.on("beforeChange", this._onBeforeChange);
this._options.editor.on("changes", this._onChanges);
this._operationQueue = [];
}
/**
* Inserts text into the editor.
*
* @param index
* The index to insert text at.
* @param text
* The text to insert.
*/
public insert(index: number, text: string): void {
this._suppress = true;
const from = this._options.editor.posFromIndex(index);
this._options.editor.replaceRange(text, from, undefined, this._options.remoteOrigin);
this._suppress = false;
}
/**
* Replaces text in the editor.
*
* @param index
* The start index of the range to replace.
* @param length
* The length of the range to replace.
* @param text
* The text to insert.
*/
public replace(index: number, length: number, text: string): void {
this._suppress = true;
const from = this._options.editor.posFromIndex(index);
const to = this._options.editor.posFromIndex(index + length);
this._options.editor.replaceRange(text, from, to, this._options.remoteOrigin);
this._suppress = false;
}
/**
* Deletes text in the editor.
*
* @param index
* The start index of the range to remove.
* @param length
* The length of the range to remove.
*/
public delete(index: number, length: number): void {
this._suppress = true;
const from = this._options.editor.posFromIndex(index);
const to = this._options.editor.posFromIndex(index + length);
this._options.editor.replaceRange("", from, to, this._options.remoteOrigin);
this._suppress = false;
}
/**
* Disposes of the content manager, freeing any resources.
*/
public dispose(): void {
this._options.editor.off("beforeChange", this._onBeforeChange);
this._options.editor.off("changes", this._onChanges);
}
/**
* A helper method to process local changes from CodeMirror. Before change
* is used because the from and to positions from code mirror are relative
* to the state of the document before the change in the "changes" event,
* but the document has already been changed. So we use before changes to
* calculate the from and to index before the document is changed. This
* method then pushes the change data into a queue that will then be
* picked up by the "change" event.
*
* @param editor
* The editor that the change originated from.
* @param changeObj
* The object that specifies the change.
*
* @internal
*/
private _onBeforeChange = (editor: Editor, changeObj: EditorChangeCancellable) => {
if (this._suppress) {
return;
}
const from = editor.indexFromPos(changeObj.from);
const to = editor.indexFromPos(changeObj.to);
let deleted: string | null = null;
let inserted: string | null = null;
// Code the from and to are the same for in inserts because the
// insert happens "at" a specific index. The delete on the other
// hand covers a non empty range.
if (from !== to) {
deleted = editor.getRange(changeObj.from, changeObj.to);
}
// If you insert a new line you will get ["", ""] in the 'text' property
// so if the first element in the text array is non-empty or if you have
// more than one element, you know you have an insert.
if (changeObj.text[0] !== "" || changeObj.text.length > 1) {
inserted = changeObj.text.join("\n");
}
this._operationQueue.push({from, to, inserted, deleted});
}
/**
* A helper method to process local changes from CodeMirror.
*
* @param _
* The editor that the change originated from.
* @param changes
* The array of changes.
*
* @private
* @internal
*/
private _onChanges = (_: Editor, changes: EditorChangeLinkedList[]) => {
if (this._suppress) {
return;
}
changes.forEach((changeObj: EditorChangeLinkedList) => {
const {from, to, inserted, deleted} = this._operationQueue.shift();
if (inserted !== null && deleted === null) {
this._options.onInsert(from, inserted);
} else if (inserted !== null && deleted !== null) {
this._options.onReplace(from, to - from, inserted);
} else if (inserted === null && deleted !== null) {
this._options.onDelete(from, to - from);
} else {
throw new Error("Unexpected change: " + JSON.stringify(changeObj));
}
});
}
}
/**
* @internal
*/
interface IOperation {
from: number;
to: number;
inserted: string;
deleted: string;
}