wed
Version:
Wed is a schema-aware editor for XML documents.
309 lines • 10.8 kB
JavaScript
define(["require", "exports", "rxjs"], function (require, exports, rxjs_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Records operations that may be undone or redone. It maintains a single list
* of [[Undo]] objects in the order by which they are passed to the
* [[UndoList.record]] method.
*
* This object maintains a single history. So if operations A, B, C, D are
* recorded, C and D are undone and then E is recorded, the list of recorded
* operations will then be A, B, E.
*/
class UndoList {
constructor() {
this.stack = [];
this.list = [];
this.index = -1;
this._undoingOrRedoing = false;
this._events = new rxjs_1.Subject();
this.events = this._events.asObservable();
}
/**
* Reset the list to its initial state **without** undoing operations. The
* list effectively forgets old undo operations.
*/
reset() {
if (this._undoingOrRedoing) {
throw new Error("may not reset while undoing or redoing");
}
this.stack.length = 0; // Yes, this works and clears the stack.
this.index = -1;
// We need to cleanup the old subscriptions.
for (const { subscription } of this.list) {
subscription.unsubscribe();
}
this.list = [];
}
/**
* This method makes the UndoList object record the object passed to it. Any
* operations that had previously been undone are forgotten.
*
* @param obj An undo object to record.
*/
record(obj) {
if (this.stack.length > 0) {
this.stack[0].record(obj);
}
else {
// We do things in reverse here. We save the original list. Then the call
// to splice mutates the original list to contain elements we do *not*
// want. The return value are those elements we do want.
const oldList = this.list;
this.list = this.list.splice(0, this.index + 1);
// We need to cleanup the old subscriptions.
for (const { subscription } of oldList) {
subscription.unsubscribe();
}
// This is the only place we need to subscribe. We do not need to
// subscribe to individual object that are in undo groups because the
// groups forward events that happen on their inner objects. Also, a group
// need not be subscribed to until ``record`` is called for it.
this.list.push({
undo: obj,
subscription: obj.events.subscribe(this._events),
});
this.index++;
}
}
/**
* Undoes the latest [[Undo]] that was recorded. If any [[UndoGroup]] objects
* were in effect when called, they are terminated. It is an error to call
* this method or [[redo]] from within this method. Does nothing if there is
* nothing to undo.
*
* @throws {Error} If an undo is attempted when an undo or redo is already in
* progress.
*/
undo() {
// If undo is invoked in the middle of a group, we must end it first.
if (this._undoingOrRedoing) {
throw new Error("calling undo while undoing or redoing");
}
this._undoingOrRedoing = true;
while (this.stack.length > 0) {
this.endGroup();
}
if (this.index >= 0) {
this.list[this.index--].undo.undo();
}
this._undoingOrRedoing = false;
}
/**
* Redoes the latest [[Undo]] object that was undone. It is an error to call
* this method or [[undo]] from within this method. Does nothing if there is
* nothing to redo.
*
* @throws {Error} If an undo is attempted when an undo or redo is already in
* progress.
*/
redo() {
if (this._undoingOrRedoing) {
throw new Error("calling redo while undoing or redoing");
}
this._undoingOrRedoing = true;
if (this.index < this.list.length - 1) {
this.list[++this.index].undo.redo();
}
this._undoingOrRedoing = false;
}
/**
* @returns True if the object is in the midst of undoing or redoing, false
* otherwise.
*/
undoingOrRedoing() {
return this._undoingOrRedoing;
}
/**
* @returns True if there is something to undo, false otherwise.
*/
canUndo() {
// If there is a group on the stack, then we have to return true. That's
// because when the group is ended when undo() is called, it will be added
// at the end of this.list.
return this.index > -1 || this.stack.length > 0;
}
/**
* @returns True if there is something to redo, false otherwise.
*/
canRedo() {
return this.index < this.list.length - 1;
}
/**
* Starts recording a group of undo operations.
*
* @param group The undo group to start.
*/
startGroup(group) {
this.stack.unshift(group);
}
/**
* Ends recording a group of undo operations. The group currently in effect is
* terminated, and made the last recorded operation (as if it had been passed
* to [[UndoList.record]]).
*
* @throws {Error} If there is no current undo group.
*/
endGroup() {
const group = this.stack.shift();
if (group === undefined) {
throw new Error("ending a non-existent group.");
}
group.end();
this.record(group);
}
/**
* Ends all groups currently in effect. This is the same as calling
* [[endGroup]] repeatedly until there are no more groups to end.
*/
endAllGroups() {
while (this.stack.length > 0) {
this.endGroup();
}
}
/**
* @returns The group currently being recorded.
*/
getGroup() {
return this.stack[0];
}
/**
* @returns A string showing all the undo steps and undo groups stored in this
* undo list.
*/
toString() {
const ret = [];
ret.push("Start of list\n");
for (const it of this.list) {
ret.push(it.toString());
}
ret.push("End of list\n");
ret.push("Unfinished groups:\n");
for (let i = this.stack.length - 1; i >= 0; --i) {
const it = this.stack[i];
ret.push(it.toString());
}
ret.push("End of unfinished groups\n");
return ret.join("");
}
}
exports.UndoList = UndoList;
/**
* An undo operation.
* @param {string} desc The description of this undo operation.
*/
class Undo {
constructor(desc) {
this.desc = desc;
this._events = new rxjs_1.Subject();
this.events = this._events.asObservable();
}
/**
* Called when the operation must be undone.
*
* @throws {Error} If an undo is attempted when an undo or redo is already in
* progress.
*/
undo() {
this.performUndo();
this._events.next({
name: "Undo",
undo: this,
});
}
/**
* Called when the operation must be redone.
*
* @throws {Error} If an undo is attempted when an undo or redo is already in
* progress.
*/
redo() {
this.performRedo();
this._events.next({
name: "Redo",
undo: this,
});
}
/**
* @returns The description of this object.
*/
toString() {
return `${this.desc}\n`;
}
}
exports.Undo = Undo;
/**
* A group of undo operations.
*/
class UndoGroup extends Undo {
constructor() {
super(...arguments);
this.list = [];
}
/**
* Undoes this group, which means undoing all the operations that this group
* has recorded.
*/
performUndo() {
for (let i = this.list.length - 1; i >= 0; --i) {
this.list[i].undo();
}
}
/**
* Redoes this group, which means redoing all the operations that this group
* has recorded.
*/
performRedo() {
for (const it of this.list) {
it.redo();
}
}
/**
* Records an operation as part of this group.
*
* @param obj The operation to record.
*/
record(obj) {
this.list.push(obj);
// We need to forward the events onto this object.
obj.events.subscribe(this._events);
}
/**
* This method is called by [[UndoList.endGroup]] when it ends a group. The
* default implementation does nothing.
*/
end() {
// by default we do nothing
}
toString() {
const ret = [];
ret.push(`Start of ${this.desc}\n`);
for (const it of this.list) {
ret.push(it.toString().replace(/(^|\n)/g, "$1 ").slice(0, -1));
}
ret.push(`End of ${this.desc}\n`);
return ret.join("");
}
}
exports.UndoGroup = UndoGroup;
/**
* This is an undo object which does nothing but only serves as a marker in the
* list of undo operations. It could be used for debugging or by modes to record
* information they need in the undo list.
*/
class UndoMarker extends Undo {
/**
* @param msg A message to identify the marker.
*/
constructor(msg) {
super(`*** MARKER *** ${msg}`);
}
// tslint:disable-next-line:no-empty
performUndo() { }
// tslint:disable-next-line:no-empty
performRedo() { }
}
exports.UndoMarker = UndoMarker;
});
// LocalWords: boolean Dubeau MPL Mangalam UndoList desc
//# sourceMappingURL=undo.js.map