@eclipse-emfcloud/model-manager
Version:
Command-based model editing with undo/redo.
399 lines (398 loc) • 18.7 kB
TypeScript
import type { Operation } from 'fast-json-patch';
import { MaybePromise } from './promises';
/**
* An union of _command_ types that can be executed on a {@link CoreCommandStack} to edit a model.
*
* @template K the type of model ID used by the Model Manager
*/
export type Command<K = string> = SimpleCommand<K> | CompoundCommand<K>;
/**
* The return result of an atomic command execution, undo, or redo is a possibly asynchronous
* delta describing the effected model changes, or nothing.
*/
export type SimpleCommandResult = MaybePromise<Operation[] | undefined>;
/**
* A command may optionally publish a `result` of its execution not in detailed JSON Patch terms but
* in an abstract result type. This interface distinguishes such commands.
*/
export interface SimpleCommandWithResult<K, R> extends SimpleCommand<K> {
/**
* The abstract return-result of the command, available once it has been successfully executed.
* The value is {@link PendingResult pending} until the command is executed.
*
* Whether the `result` value remains available or valid after the command is undone or redone
* is not specified. In general, it is recommended only to access this result after the initial
* execution of the command.
*
* The `result` property should be initialized to a {@link PendingResult} so that the command
* may be {@linkplain isSimpleCommandWithResult recognized as providing the result} eventually
* after execution.
*
* @see {@link SimpleCommand.execute}
*/
readonly result: CommandReturnResult<R>;
}
/**
* The specific return-result of a {@link SimpleCommandWithResult} that provides
* an application-specific representation of the command's accomplished effect.
*/
export type CommandReturnResult<R> = ReadyResult<R> | PendingResult | FailedResult;
/**
* Representation of a simple command result that is ready following
* successful execution of the command.
*/
export interface ReadyResult<R> {
status: 'ready';
value: R;
}
/**
* Representation of a simple command result that is not yet ready because
* execution of the command has not been completed (or perhaps not even started).
*/
export interface PendingResult {
status: 'pending';
}
/**
* Representation of a simple command result that is not and will not be ready because
* execution of the command has failed.
*/
export interface FailedResult {
status: 'failed';
/** An optional error message, compatible with common `Error` types. */
error?: {
message: string;
};
}
/**
* Query whether a command is a simple command publishing an abstract execution result.
*/
export declare const isSimpleCommandWithResult: <K, R>(command: Command<K>) => command is SimpleCommandWithResult<K, R>;
/**
* Unwrap the return result of a {@link SimpleCommandWithResult}.
*
* @returns the return-result `value` if it is `ready`, otherwise `undefined` if it is
* still `pending`
* @throws the return-result `error` if it is `failed`
*/
export declare const unwrapReturnResult: <K, R>(command: SimpleCommandWithResult<K, R>) => R | undefined;
/**
* An operation that edits a model and may (conditionally) be undone to
* revert its changes and redone to restore its changes.
*
* @template K the type of model ID used by the Model Manager
*/
export interface SimpleCommand<K = string> {
/**
* A label for the command that may be presented to the user in an UI, used to identify it in logs, etc.
*
* @readonly
*/
readonly label: string;
/**
* The model affected by this command.
*
* @readonly
*/
readonly modelId: K;
/**
* Query whether any preconditions that I may have for viable execution are met.
* On a `true` result, I guarantee that I can effect my changes on the model correctly and completely.
* Otherwise, it is an error to attempt to {@link execute} me.
*
* @param model the model on which I am to be executed
* @returns whether I am able to be executed
*/
canExecute(model: object): MaybePromise<boolean>;
/**
* Query whether any preconditions that I may have for viable undo are met.
* On a `true` result, I guarantee that I can revert my previously executed changes on the model correctly and completely.
* Otherwise, it is an error to attempt to {@link undo} me.
*
* @param model the model on which I am to be undone
* @returns whether I am able to be undone
*/
canUndo(model: object): MaybePromise<boolean>;
/**
* Query whether any preconditions that I may have for viable redo are met.
* On a `true` result, I guarantee that I can re-apply my previously undone changes on the model correctly and completely.
* Otherwise, it is an error to attempt to {@link redo} me.
*
* @param model the model on which I am to be redone
* @returns whether I am able to be redone
*/
canRedo(model: object): MaybePromise<boolean>;
/**
* Perform my changes on the model.
*
* If the changes that I make can be expressed as a JSON patch, the result is that patch.
* In this case, the patch is applicable to the "before state" of the model, the state it was in before I was executed.
*
* @param model the model on which I am to be executed
* @returns a JSON patch describing the changes that I performed, if they can be described in those terms
* @throws if I am {@link canExecute not executable} according to my preconditions
*/
execute(model: object): SimpleCommandResult;
/**
* Revert the changes that I had previously {@link execute}d on the model.
*
* If the changes that I make in this reversion can be expressed as a JSON patch, the result is that patch.
* In this case, the patch is applicable to the "before state" of the model, the state it was in before I was undone
* and therefore by implication the state it was in after I was originally executed.
*
* @param model the model on which I am to be undone
* @returns a JSON patch describing the changes that I performed, if they can be described in those terms
* @throws if I am {@link canUndo not undoable} according to my preconditions
*/
undo(model: object): SimpleCommandResult;
/**
* Restore the changes that I had previously {@link undo}ne on the model.
*
* If the changes that I make in this restoration can be expressed as a JSON patch, the result is that patch.
* In this case, the patch is applicable to the "before state" of the model, the state it was in before I was redone
* and therefore by implication the state it was in after I was originally undone.
*
* @param model the model on which I am to be redone
* @returns a JSON patch describing the changes that I performed, if they can be described in those terms
* @throws if I am {@link canRedo not redoable} according to my preconditions
*/
redo(model: object): SimpleCommandResult;
}
/**
* The result of a compound command is a promised (future)
* mapping of deltas describing the effected model changes of each
* constituent atomic command, or nothing.
*/
export type CompoundCommandResult<K = string> = Promise<Map<Command<K>, Operation[]> | undefined>;
type GetModel<K> = (modelId: K) => object | undefined;
/**
* An operation that edits a model by composition of one or more steps implemented by commands
* and that may (conditionally) be undone to revert its changes and redone to restore its changes.
*
* Compound commands are iterable, returning their constituent leaf simple commands in forward
* execution order. Iteration is depth-first over a tree of nested compounds.
*
* @template K the type of model ID used by the Model Manager
* @interface CompoundCommand
*/
export interface CompoundCommand<K = string> extends Iterable<SimpleCommand<K>> {
/**
* A label for the command that may be presented to the user in an UI, used to identify it in logs, etc.
*
* @readonly
*/
readonly label: string;
/**
* Append some number of commands to the list that I will {@link execute}.
* Once I have been executed, my commands are frozen and may no longer be appended.
*
* @param commands commands to append to me
* @returns myself, for convenience of call chaining
* @throws if I have already been {@link execute}d
*/
append(...commands: Command<K>[]): this;
/**
* Query the commands that comprise me, in the order in which they are executed.
* The returned array is a copy and may freely be modified by the caller.
*
* @returns my constituent commands
*/
getCommands(): Command<K>[];
/**
* Query whether any preconditions that I may have for viable execution are met.
* At a minimum, this is a conjunction of the executability of my constituent commands.
*
* On a `true` result, I guarantee that I can effect my changes on the model correctly and completely.
* Otherwise, it is an error to attempt to {@link execute} me.
*
* @param getModel a function providing the models on which I shall execute my sub-commands
* @returns whether I am able to be executed
*/
canExecute(getModel: GetModel<K>): Promise<boolean>;
/**
* Query whether any preconditions that I may have for viable undo are met.
* At a minimum, this is a conjunction of the undoability of my constituent commands.
*
* On a `true` result, I guarantee that I can revert my previously executed changes on the model correctly and completely.
* Otherwise, it is an error to attempt to {@link undo} me.
*
* @param getModel a function providing the models on which I shall undo my sub-commands
* @returns whether I am able to be undone
*/
canUndo(getModel: GetModel<K>): Promise<boolean>;
/**
* Query whether any preconditions that I may have for viable redo are met.
* At a minimum, this is a conjunction of the redoability of my constituent commands.
*
* On a `true` result, I guarantee that I can re-apply my previously undone changes on the model correctly and completely.
* Otherwise, it is an error to attempt to {@link redo} me.
*
* @param getModel a function providing the models on which I shall redo my sub-commands
* @returns whether I am able to be redone
*/
canRedo(getModel: GetModel<K>): Promise<boolean>;
/**
* Perform my changes on the model by execution, in forward order, of my constituent commands.
*
* If the changes that I make can be expressed as a JSON patch, the result is that patch.
* In this case, the patch is applicable to the "before state" of the model, the state it was in before I was executed.
*
* @param getModel a function providing the models on which I shall execute my sub-commands
* @returns a mapping of JSON patches describing the changes performed by my constituent commands,
* if they can be described in those terms
* @throws if I am [not executable]{@link canExecute} according to my preconditions
*/
execute(getModel: GetModel<K>): CompoundCommandResult<K>;
/**
* Revert the changes that I had previously {@link execute}d on the model by undo, in reverse order, of my constituent commands.
*
* If the changes that I make in this reversion can be expressed as a JSON patch, the result is that patch.
* In this case, the patch is applicable to the "before state" of the model, the state it was in before I was undone
* and therefore by implication the state it was in after I was originally executed.
*
* @param getModel a function providing the models on which I shall undo my sub-commands
* @returns a mapping of JSON patches describing the changes that I performed, if they can be described in those terms
* @throws if I am [not undoable]{@link canUndo} according to my preconditions
*/
undo(getModel: GetModel<K>): CompoundCommandResult<K>;
/**
* Restore the changes that I had previously {@link undo}ne on the model by redo, in forward order, of my constituent commands.
*
* If the changes that I make in this restoration can be expressed as a JSON patch, the result is that patch.
* In this case, the patch is applicable to the "before state" of the model, the state it was in before I was redone
* and therefore by implication the state it was in after I was originally undone.
*
* @param getModel a function providing the models on which I shall redo my sub-commands
* @returns a mapping of JSON patches describing the changes that I performed, if they can be described in those terms
* @throws if I am [not redoable]{@link canRedo} according to my preconditions
*/
redo(getModel: GetModel<K>): CompoundCommandResult<K>;
/**
* Iterate my constituent commands in execution order, applying a processor to each.
*
* @param processor the processor function
*/
forEach(processor: (item: SimpleCommand<K>, index: number) => void): void;
/**
* Iterate my constituent commands in execution order, applying a transformation to
* each and returning the results.
*
* @template T the result type of the transformation function
*
* @param transform the transformation function
* @returns the transformation results
*/
map<T>(transform: (item: SimpleCommand<K>, index: number) => T): T[];
}
/**
* An actually asynchronous {@linkplain CompoundCommandResult compound command result}.
*/
export type CommandResult<K = string> = Promise<Map<Command<K>, Operation[]> | undefined>;
/**
* A private enumeration of the states of a `CompoundCommand`, used
* for precondition checking on all of its operations.
*
* @enum State
*/
type State = 'ready' | 'executed' | 'undone';
/**
* A basic implementation of the `CompoundCommand` interface suitable for most uses.
*
* @class CompoundCommandImpl
*/
export declare class CompoundCommandImpl<K = string> implements CompoundCommand<K> {
protected readonly _label: string;
protected readonly _commands: Command<K>[];
protected state: State;
constructor(label: string, ...commands: Command<K>[]);
get label(): string;
/** Query whether I am in the "ready" (not yet executed, undone, or redone) state. */
protected isReady(): boolean;
canExecute(getModel: GetModel<K>): Promise<boolean>;
/** Query whether I am in the "executed" (or redone) state. */
protected wasExecuted(): boolean;
canUndo(getModel: GetModel<K>): Promise<boolean>;
/** Query whether I am in the "undone" state. */
protected wasUndone(): boolean;
canRedo(getModel: GetModel<K>): Promise<boolean>;
execute(getModel: GetModel<K>): CommandResult<K>;
undo(getModel: GetModel<K>): CommandResult<K>;
redo(getModel: GetModel<K>): CommandResult<K>;
append(...commands: Command<K>[]): this;
getCommands(): Command<K>[];
/**
* Compute the conjunction of a possibly asynchronous `predicate` over all of my constituent commands.
*
* @param predicate a test to apply to each command in turn
* @returns the conjunction of the `predicate` results for every command, or `false` if I have no commands
*/
protected everyCommand(predicate: (command: Command<K>) => MaybePromise<boolean>): Promise<boolean>;
/**
* Query whether I am in some `state`.
*
* @param state a state to query
* @returns whether I am in the given `state`
*/
private inState;
/**
* Guard the invocation of some operation by preconditions of `state` and other
* arbitrary conditions.
*
* @param op the name of the operation being guarded
* @param state the state in which I must be for valid invocation of the operation
* @param canDo whether other preconditions for the operation are met
*
* @throws if either I am not in the given `state` or `canDo` is `false`
*/
private checkState;
[Symbol.iterator](): Iterator<SimpleCommand<K>>;
forEach(processor: (item: SimpleCommand<K>, index: number) => void): void;
map<T>(transform: (item: SimpleCommand<K>, index: number) => T): T[];
/**
* Iterate over my commands, applying an execute/undo/redo operation on each, and collecting the results.
*
* The iteration order depends on my current state: if I have been executed or redone, then iteration is backwards
* from my last subcommand to my first, because the operation that I can perform is an undo.
* Otherwise, the order is forwards from my first subcommand to my last.
*
* The result is `undefined` if all of my subcommands returned an `undefined` result.
* Otherwise, it is a mapping of subcommands to their results, for those that returned some result.
*
* @param operation the operation to invoke on my commands
* @param getModel a function providing the models on which I shall execute/undo/redo my sub-commands
* @returns the aggregate results of the `operation` over my commands, if available
*/
private iterateCommands;
}
/**
* Type guard determining whether a `command` is a `CompoundCommand`.
*
* @function
* @param command a command
* @returns whether the `command` is a `CompoundCommand`
*/
export declare const isCompoundCommand: <K = string>(command: Command<K>) => command is CompoundCommand<K>;
/**
* Append zero or more commands to a `base` command.
*
* If the `commands` to append are none, then the `base` is returned as is.
*
* If the `base` command is already a compound, it is appended in place and returned.
* Otherwise, a new `CompoundCommand` is created from all of the given commands and
* returned.
*
* In all cases, the label of the result is the label of the `base` command.
*
* @template K the type of model ID used by the Model Manager
* @param base the command on which to append one or more additional commands
* @param commands commands to append
* @returns a compound of the `base` and, `commands`, in that order
*/
export declare const append: <K = string>(base: Command<K>, ...commands: Command<K>[]) => Command<K>;
/**
* Convert a map of (Command, Operation[]) to a map of (Model, Operation[]).
* @param commands The map to convert (typically the result of an execute/undo/redo call)
* @returns A map grouping result Operation[] by affected model
*/
export declare const groupByModelId: <K = string>(commands: Map<Command<K>, Operation[]>) => Map<K, Operation[]>;
export {};
//# sourceMappingURL=command.d.ts.map