@eclipse-emfcloud/model-manager
Version:
Command-based model editing with undo/redo.
487 lines • 21.5 kB
TypeScript
import { Operation } from 'fast-json-patch';
import { Command, CompoundCommandImpl, CoreCommandStack, CoreCommandStackSubscription, DirtyStateChangedCallback, EditingContext, EditingContextChangedCallback, MaybePromise, RedoAnalysis, UndoAnalysis, UndoRedoOp } from '../core';
/** Any operation that can be performed on a command. */
export type CommandOp = 'execute' | UndoRedoOp;
export interface WorkingCopyManager<K = string> {
/**
* Prepare a new edit session in which working copies will be required.
*/
open(modelId: K[]): void;
/**
* Whether the working copy manager has an open session.
*/
isOpen(modelId: K[]): boolean;
/**
* Get a working copy of the model identified by the given ID, if it
* exists. Until changes are committed or the working copies are
* reset by a call to {@link open}, the same working copy must be
* supplied for all requests for the same ID.
*/
getWorkingCopy(modelId: K): object | undefined;
/**
* Get the current public state of the model identified by the given ID, if it
* exists. **Note** that this **is not a working copy**.
*/
getModel(modelId: K): object | undefined;
/**
* Optional method to follow up changes
* performed on models with further changes to be executed in
* isolation, not on the command history.
* The purpose of such changes must only be to ensure integrity of
* dependencies between properties of the models changed in this
* `commandResult`.
* The hook, if defined, is called following every execution, undo,
* and redo of any command, and so must be prepared to logically
* invert changes that it had provided previously to whatever extent
* makes sense for its models. It is for this reason that the changes
* provided by this hook are not recorded in the history, because
* the hook is invoked also on undo/redo, not just on the original
* execution of every command.
*/
createFollowUpCommand?(commandResult: Map<Command<K>, Operation[]>): MaybePromise<Command<K> | undefined>;
/**
* Commit the current working copy state back to the model storage,
* with a summary of the changes that are being committed.
*
* @param result a map of commands to the operations that were executed
* @param modelIds the model IDs that were opened
*/
commit(result: Map<Command<K>, Operation[]>, modelIds: K[]): void;
/**
* Discard the current working copy state and close.
*/
cancel(modelIds: K[]): void;
}
export declare class CoreCommandStackImpl<K = string> implements CoreCommandStack<K> {
private workingCopyManager;
/** The most recent of all commands executed, in temporal order, in all editing contexts. */
private _top?;
/**
* A pointer for each editing context to the top of its undo stack, keyed by editing context ID.
* Use key type `string`, not `EditingContext`, to be explicit in case of any future evolution
* of the editing context as a complex type with a string identifier.
*/
private _undoEntries;
/**
* A pointer for each editing context to the top of its redo stack, keyed by editing context ID.
* @see {@link _undoEntries}
*/
private _redoEntries;
/**
* A pointer to the stack entry that is the last command executed or redone in each context
* at the time that context was last marked saved.
* If a context does not have an entry then either it was never saved or it was saved when
* no commands have been executed/redone, which makes no difference.
* If the mapping is the flush token, then the context was dirty at the time it was flushed.
*/
private _savepoints;
/**
* Subscriptions by editing context, with the `null` key track subscriptions to all contexts.
*/
private _subscriptions;
/**
* Exclusively execute all public operations on the stack.
*/
private _exclusive;
/**
* Operations to run after committing a working copy transaction.
*/
private postCommitOperations;
/**
* Constructor of the core command stack.
*
* @param notify a call-back that receives the notification of changes
* applied to one or more models, to broadcast to subscriptions or
* to process otherwise
* @param createFollowUpCommand Optional hook to follow up changes
* performed on models with further changes to be executed in
* isolation, not on the command history.
* The purpose of such changes must only be to ensure integrity of
* dependencies between properties of the models changed in this
* `commandResult`.
* The hook, if defined, is called following every execution, undo,
* and redo of any command, and so must be prepared to logically
* invert changes that it had provided previously to whatever extent
* makes sense for its models. It is for this reason that the changes
* provided by this hook are not recorded in the history, because
* the hook is invoked also on undo/redo, not just on the original
* execution of every command.
*/
constructor(workingCopyManager: WorkingCopyManager<K>);
execute(command: Command<K>, ...contexts: EditingContext[]): Promise<Map<Command<K>, Operation[]> | undefined>;
/**
* Perform an `operation` on a `command`.
*
* @param operation whether to execute, undo, or redo the `command`
* @param command the command to execute, undo, or redo
* @return a description of the model changes resulting from the execution, undo, or redo of the `command`
*/
private perform;
executeAndAppend(appendedContext: EditingContext, command: Command<K>, ...contexts: EditingContext[]): Promise<Map<Command<K>, Operation[]> | undefined>;
undo(context: EditingContext, withDependencies?: boolean): Promise<Map<Command<K>, Operation[]> | undefined>;
/**
* Revert the command on the top of the undo or redo stack of an editing `context`
* according to the indicated `op`eration.
*
* @param context the editing context to undo or redo
* @param op whether to revert the top of the undo or the redo stack of the `context`
* @param withDependencies whether first to revert dependencies of the `context` as necessary
*
* @see {@link undo}
* @see {@link redo}
*/
private performUndoRedo;
/**
* Merge two stack entries and remove the one that was merged into the other from
* the history as now it is included in the other.
*
* @param into the stack entry into which to merge the other
* @param from the other entry to merge `into` the first
* @param forOp whether the merge is for undo or redo, which determines the order
* in which the commands of the `from` entry are merged `into` the other's commands
* @returns the result of the merge, which is just the `into` entry that now has been increased
*
* @see {@link StackEntry.merge}
*/
private merge;
redo(context: EditingContext, withDependencies?: boolean): Promise<Map<Command<K>, Operation[]> | undefined>;
/**
* Perform an `operation` on command(s) in my history that themselves modify working
* copies of models supplied by the given working copy manager.
* On successful conclusion of the `operation` the working copies that it used are committed
* to the model manager; on failure they are just abandoned to retain the prior state of
* all managed models.
* In the event that the working copies are committed, all post-commit operations gathered
* during the `operation` are executed; otherwise they too are abandoned.
*
* @param workingCopyManager the model manager's working-copy manager
* @param operation the model operation to perform on working copies
*/
protected withWorkingCopies<K>(workingCopyManager: WorkingCopyManager<K>, operation: () => Promise<Map<Command<K>, Operation[]> | undefined>, modelIds: K[]): Promise<Map<Command<K>, Operation[]> | undefined>;
/**
* Gather an operation to run when model working copies are committed
* to the model manager.
*
* @param closer an operation to run on working copy commit
*/
protected afterCommit(closer: () => unknown): void;
/**
* Perform an `operation` on command(s) in my history with tracking and subsequent notification
* of changes in the dirty state of models. Upon successful completion of the `operation` and
* committing of its working copies to the model manager, dirty state change subscriptions
* will be notified.
*
* @param contexts the editing contexts in which the `operation` is performed
* @param operation the model operation to perform that will potentially change dirty states
*/
protected withDirtyNotification<T>(contexts: EditingContext[], operation: () => Promise<T>): Promise<T>;
/**
* Perform all post-change processing steps, including at least
*
* 1. Run the follow-up hook, if defined
* 2. Run the notify hook
*
* if and only if the `commandResult` is defined.
*
* @param commandResult the result of a command execution, undo, or redo
* @param the processed command result
*/
private postChange;
/**
* Call my follow-up hook, if defined, and return the results of executing the
* command that it provides.
*
* @param commandResult the command result to inject into the follow-up hook
* @returns the results from the follow-up hook's provided command, if any
*/
private processFollowUp;
canExecute(command: Command<K>, ...contexts: string[]): Promise<boolean>;
canUndo(context: EditingContext, withDependencies?: boolean): Promise<boolean>;
canRedo(context: EditingContext, withDependencies?: boolean): Promise<boolean>;
getUndoCommand(context: EditingContext): Command<K> | undefined;
getRedoCommand(context: EditingContext): Command<K> | undefined;
flush(context: EditingContext): Command<K>[];
/**
* Add the stack entry for a command that has been executed in one or more contexts.
*
* @param entry the stack entry to add
*/
private executed;
/**
* Merge a command that has been executed into the stack-entry for the command that it appended.
*
* @param appendedEntry the entry for the command that was appended to
* @param newEntry an entry for the command that was appended
*/
private appended;
/**
* Flush all redo stacks for the editing contexts of an `entry`.
* This is used after executing a command whose `entry` this is.
*
* @param entry an entry that has just been executed
*/
private flushRedo;
/**
* Flush the redo stack starting from a given entry all the way down to the bottom.
* All editing contexts of the starting entry are flushed from that point.
* Thus some contexts may still have some older redo entries remaining.
*
* @param startingAt the top of the redo stack to flush
*/
private flushRedoFrom;
/**
* Push a new entry onto the stack for a command that was executed.
*
* @param entry an entry to push onto the stack
* @returns the `entry` that was pushed
*/
private push;
/**
* Remove an entry from wherever in the stack it occurs. So, no strictly speaking
* a "pop" which is a LIFO operation, but it's the converse of our {@link push}.
*
* @param entry an entry to remove from the stack
* @returns the stack entry that remains at the point where the `entry` was removed,
* if the `entry` was not the very first in the temporal order
*/
private pop;
/**
* Update the undo pointers for all editing contexts of an `entry` whose
* command has been undone.
*
* @param entry the entry for a command that was undone
*/
private undone;
/**
* Update the redo pointers for all editing contexts of an `entry` whose
* command has been redone.
*
* @param entry the entry for a command that was redone
*/
private redone;
/**
* Get the entry for the command that is at the top of an editing context's
* undo stack or redo stack, that being the next command to undo or redo in
* the context.
*
* @param editingContext the editing context
* @param op whether to retrieve the undo or the redo entry
* @returns the entry for the command at the top of the context's undo or redo stack
*/
private getEntry;
/**
* Assert that a command is executable.
* Precondition for executing it on the stack.
*
* @param command a command to be executed
* @param contexts the editing contexts in which it is to be executed
* @throws if the `command` cannot be executed
*/
private checkExecute;
/**
* Test whether an operation on some `command` would be permitted in its current state.
*
* @param condition whether the `canExecute`, `canUndo`, or `canRedo` condition of the command is to be tested
* @param command the command to be tested
* @return the result of the test of the `command`'s current state under the `condition`
*/
private test;
/**
* Assert that a command is revertible for undo or redo.
* Precondition for undoing or redoing it on the stack.
*
* @param entry the entry for a command to be reverted
* @param op the undo/redo operation to be validated
* @param withDependencies whether to allow dependencies in the undo
* @throws if the command of the `entry` cannot be reverted
* @see {@link canUndoRedo}
*/
private checkUndoRedo;
/**
* Query whether a command is revertible for undo or redo.
*
* A command may intrinsically be non-revertible or it may not be revertible because
* one of its contexts has a command that would need to be reverted before it.
*
* @param entry the entry for a command to be revered
* @param op whether the reversion is an undo or a redo
* @param withDependencies whether to allow dependencies in the undo. Default `false`
* @returns whether the command of the `entry` can be reverted
*/
private canUndoRedo;
analyzeUndo(context: string): Promise<UndoAnalysis>;
/**
* Compute the detailed analysis of the undoability or redoability of an editing `context`.
*
* @param context the editing context to analyze
* @param op whether to analyze the feasibility of `undo` or `redo` of the `context`
* @return the detailed analysis report
*/
private analyzeUndoRedo;
/**
* Get the stack entries encoding dependencies, if any, of the given editing `context`
* for undo or redo.
*
* @param context an editing context for which to get dependencies
* @param op whether the analysis is for `undo` or for `redo`
* @return the stack entries, or an empty array if none, that must be undone or
* redone in other contexts before the given `context` can be undone or redone
*/
private getDependencyEntries;
analyzeRedo(context: string): Promise<RedoAnalysis>;
markSaved(editingContext: EditingContext): void;
isDirty(editingContext: string): boolean;
getDirtyModelIds(editingContext: EditingContext): K[];
getEditingContexts(): EditingContext[];
subscribe(editingContext?: string | undefined): CoreCommandStackSubscription<K>;
/**
* Invoke the `onContextChanged` call-backs of subscriptions that have it.
*
* @param args the call-back arguments to pass along
*/
protected notifyCommandStackChanged(...args: Parameters<EditingContextChangedCallback<K>>): void;
/**
* Get all subscriptions pertaining to the given editing context.
*
* @param editingContext an editing context
* @returns the subscriptions targeting the context specifically and all contexts generally
*/
private getSubscriptions;
/**
* Invoke the `onDirtyStateChanged` call-backs of subscriptions that have it.
*
* @param args the call-back arguments to pass along
*/
protected notifyDirtyStateChanged(...args: Parameters<DirtyStateChangedCallback<K>>): void;
}
/**
* The command stack is a doubly-linked list, in temporal order of their original execution, of
* commands and their associated editing contexts.
* This class implements a node in that list.
*/
export declare class StackEntry<K = string> {
/** The editing contexts in which a command was executed. */
private readonly _editingContexts;
/** The command that was executed. */
private _command;
/** The next command in the list (stack). */
private _next?;
/** The previous command in the list (stack). */
private _previous?;
constructor(command: Command<K>, editingContexts: EditingContext[]);
/** Get the command that was executed. */
get command(): Command<K>;
/** Get the editing contexts in which the command was executed. */
get editingContexts(): Set<EditingContext>;
/**
* Query whether the entry may be purged because all of its
* editing contexts have been flushed.
*/
get isPurgeable(): boolean;
/**
* Obtain the next command in the stack in temporal order,
* regardless of editing contexts.
*/
get next(): StackEntry<K> | undefined;
/**
* Obtain the next command in the stack that has the given
* editing context.
*
* @param editingContext an editing context
* @returns the next command, if any, in that context
*/
nextIn(editingContext: EditingContext): StackEntry<K> | undefined;
/**
* Does this entry precede an`other` in the history?
* An entry does not precede itself.
*
* @param other another stack entry
* @returns whether the `other` is in my {@link next next chain}
*
* @see {@link next}
*/
precedes(other: StackEntry<K>): boolean;
/**
* Obtain the previous command in the stack in temporal order,
* regardless of editing contexts.
*/
get previous(): StackEntry<K> | undefined;
/**
* Obtain the previous command in the stack that has the given
* editing context.
*
* @param editingContext an editing context
* @returns the previous command, if any, in that context
*/
previousIn(editingContext: EditingContext): StackEntry<K> | undefined;
/**
* Does this entry succeed an`other` in the history?
* An entry does not succeed itself.
*
* @param other another stack entry
* @returns whether the `other` is in my {@link previous previous chain}
*
* @see {@link previous}
*/
succeeds(other: StackEntry<K>): boolean;
/**
* Remove editing contexts (that were flushed) from a command.
*
* @param editingContexts the editing contexts to remove from the command
*/
removeContext(...editingContexts: EditingContext[]): void;
/**
* Query whether my command is associated with the given editing context.
*
* @param editingContext an editing context
* @returns whether the command has that context
*/
hasContext(editingContext: EditingContext): boolean;
/**
* Insert a new entry into the stack following me, making it my new next.
* My former next becomes its next and I become its previous.
*
* @param next my new next entry
* @returns the `next` entry that was pushed
* @throws on attempt to push the entry, itself, to make a cycle
*/
push(next: StackEntry<K>): StackEntry<K>;
/**
* Remove me from the stack.
*
* @returns my former previous entry
*/
pop(): StackEntry<K> | undefined;
/**
* Merge an `entry` into me, appending its command to mine
* and its editing contexts to mine.
*
* @param entry an entry to merge into me
* @param forOp the operation for which the compound is being prepared
* @returns me
*/
merge(entry: StackEntry<K>, forOp?: UndoRedoOp): this;
}
/**
* A specialized compound command that allows appending an already-executed command
* if the compound has already been executed but not undone.
*/
export declare class AppendableCompoundCommand<K = string> extends CompoundCommandImpl<K> {
/** Construct an appendable compound that is initially in the _executed_ state. */
constructor(label: string, ...commands: Command<K>[]);
/** Construct an appendable compound that is initially in the _executed_ state. */
constructor(label: string, initialState: CompoundCommandImpl['state'], ...commands: Command<K>[]);
append(...commands: Command<K>[]): this;
}
/**
* Compose a `base` command with additional `commands` for either undo.
* **Note** that compounding commands for their initial execution does not need
* this specialized mechanism.
*
* @param base a command to append to
* @param commands commands to append to it
* @returns some command that includes the `base` and all of the additional `commands`
*/
export declare const append: <K = string>(base: Command<K>, ...commands: Command<K>[]) => Command<K>;
export declare function getModelIds<K>(command: Command<K>): K[];
//# sourceMappingURL=core-command-stack-impl.d.ts.map