UNPKG

@eclipse-emfcloud/model-manager

Version:

Command-based model editing with undo/redo.

399 lines (398 loc) 18.7 kB
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