UNPKG

@eclipse-emfcloud/model-manager

Version:

Command-based model editing with undo/redo.

487 lines 21.5 kB
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