Generalized Undo Support in Eclipse

Last updated:  Nov 9, 2004

Status:  Under investigation (see bug 37716)

Problem Description

In R3.0.1 of the Eclipse SDK, there is no generalized support for undoing user actions.  Each plug-in is left to implement its own strategy for undo, if at all.  This approach can cause many general problems as independently developed plug-ins implement their own undo strategies:

 

-          Undo and redo actions appear in different menus.

-          Undo and redo implementations can have different semantics.  Different approaches to undo by different plug-ins may confuse the user if they choose different semantics.  For example:

o        Is undo strictly linear?

o        If it is not linear, what techniques are used to undo an operation?  Implementation models for non-linear undo (rollback and perform the history again, dynamic reversal) can affect the user outcome.

o        What is the scope of an undo?  Is it global to the system?  Is it local to the window?  Is it model-dependent? 

-          Plug-ins that want to add undo support may have to choose between different undo strategies and implementations provided by their required plug-ins, or worse yet, implement yet another strategy.

Challenges with generalized undo support

While implementation of a centralized undo mechanism should be straightforward, it has not been provided in previous releases due to problems in defining the proper undo semantics across different plug-ins.   The challenge in defining consistent, predictable undo semantics can best be understood by looking at the two different undo models provided in the R3.0.1 release of the base Eclipse SDK. 

 

The SDK currently contains two undo implementations and user commands:

 

-          Text undo (Edit>Undo) provides a linear undo model for text-level operations.  That is, textual insert/replace/delete operations can be undone in the reverse order in which they are applied. 

-          Refactoring undo (Refactor>Undo) allows an undo of the last refactoring operation, but only when the workspace has not been otherwise modified since the last refactoring.

 

In discussing the appropriate semantics for workbench undo, it is useful to introduce terminology that is independent of any particular implementation for undo support.

Undo contexts

Undo implementations must provide a history of operations performed by the user and some knowledge of the “context” for these operations.  For example, the text editor undo operates within the context of a single document, while refactoring undo operates within the context of some workspace resources.  

 

The context for an operation helps determine where the undo for that operation is available.  When the contexts for different operations are completely separate, the semantics for undoing an operation from either context are easily understood.  Undoing an operation from one context does not affect the operation history from the other context.

Undo scopes

Even when completely separate undo contexts exist, the scope of the operation history and resulting undo operation must be defined.  There are a few different alternatives:

 

-          The UI that manipulates each separate undo context provides its own undo command.  This is done in R3.0.1 with Edit>Undo and Refactoring>Undo.  This does not scale well, but makes it clear which context is to be undone by forcing the user to explicitly choose the context for the undo.  We call this an explicit local undo scope.

-          A unified undo command exists and its meaning is interpreted according to some current context (such as the active part or the currently edited model).  The unified command only ever operates on the current context.  This is analogous to retargeted global actions in the Eclipse workbench.  We call this implicit local undo scope.

-          A unified undo command exists and its meaning is global for all operations performed in the system.  The history list and default operation to be undone have no relationship to the user’s current context.  We call this global undo scope.

 

The appropriate scope to be used depends on what the user is doing.  When the operations performed by a user are sequential steps in a larger task, a global scope may be appropriate.  When a user is performing separate tasks while working in separate views, a more localized scope is appropriate.  The decision is largely subjective and often cannot be predicted by the system. [1]

Shared undo contexts

Unfortunately, the SDK model for undo is more complicated.  The contexts for edit and refactoring undo are not completely separate.  The user can trigger refactoring changes from the editor itself.  These changes can manipulate both the document being edited and other workspace resources.  Further, these changes occur alongside localized text editing changes, creating an operation history with multiple contexts that are initiated from a single part. 

 

Even when a local undo scope is employed, there are different possibilities for interpreting “undo” when a refactoring operation is triggered from an editor:

 

  1. Edit>Undo causes reversal of the refactoring operation that was triggered inside the editor.
  2. Edit>Undo causes reversal of only the editor’s text-related changes that were caused by the refactoring operation.  Thus, the text editing caused by the refactor is undone, but the refactor itself is not undone (this happens in R3.0.1).
  3. Edit>Undo is unaware of the refactoring operation since it affected a context larger than the editor itself.  The text operation that occurred before the refactor is undone.

 

Examining scenarios and the desired behavior shows that any one (or more) of the three alternatives are desirable at different times, depending on what the user is trying to achieve.  Appendix A collects various refactoring and edit scenarios to help demonstrate the challenge.

Linear vs. selective undo

The appearance of a simple Edit>Undo command in the UI typically implies a linear undo.  That is, the most recent operation executed is undone.  Descriptive undo labels (such as “Undo Typing” or “Undo Rename”) help to remind the user of the last “undoable” operation.

 

As we’ve shown in discussing scopes and contexts, a strictly linear global undo is not the ideal behavior when working across contexts.  Once an attempt is made to choose the “appropriate” operation to be undone in the user’s current context, it’s possible that the chosen operation is not the most recently executed operation.  Decisions must be made about how to handle more recently executed operations.  A strictly linear model would force more recent operations to be undone before allowing the chosen operation to be undone.

 

In a selective undo approach, users are given an opportunity to view the history list and select the operation to be undone.  Operations that can be undone in the current state of the application are available for undo at any time, even if there are more recent operations in the history. 

 

The workbench support should minimally be a hybrid of these approaches.  When the operation appropriate for one context is not the most recently executed one, then the user (by prompt or by preference) should have input into the decision to roll back any more recent operations or perform the undo selectively and retain the rest of the history list.

Proposed Undo Support

While the discussion and scenarios surrounding scopes and contexts have focused on the “hard cases,” it should be realized that most users will seek out “undo” in the simple cases.  Single context, strictly linear undo, particularly in text editing, is provided in most popular applications.  The availability of a more selective undo for the “hard cases” should not hinder the expected behavior of the simple cases.

 

Support for undo should be “pay as you go,” in terms of efficiency, burden on the user, and burden on the implementer.  Validation of operations in the history list against the current state of the application should be on demand, rather than forcing operation objects to watch the current state of the application and react to changes (even when the user might never choose “undo.”)

 

Undo support should not assume the presence of a user interface.  The operations history and associated interfaces can be defined independent of the UI.

 

With these goals in mind, the following implementation is proposed:

Operations Framework

A framework for defining executable, undoable operations (IOperation) will be provided in a core package, org.eclipse.core.operations.  As actions are run, an object describing the operation to be performed is created, executed, and added to an operations history (IOperationHistory).  Operations which are comprised of distinct steps are represented as compound operations.  Compound operations must be executed as a unit, and can never be partially undone. 

 

Operations can be assigned one or more contexts (IOperationContext) to which they apply.  The interface for operation contexts is left very general so that operation implementers may choose the appropriate representation for their contexts.  In some cases, a part-oriented context may be appropriate (an editor’s undo context is the editor part itself).  In other cases, a model object may serve as the context (the navigator’s context is the workspace). The interface leaves this to the discretion of the implementer and requires only that equality of contexts can be established.  User-appropriate labels and descriptions for operation contexts are required so that any filtered operation history views can appropriately label the contexts that are filtering the view.

 

Contexts can be assigned in multiple ways:

1.       When the triggering object knows the context of the operation, the context can be assigned up front.  For example, text operations are triggered by typing in the editor, and the editor can assign its context to the operation before adding it to the history. 

2.       Depending on the structure of the code, the object that creates the operation object may not know what context is appropriate, or if other contexts should be included.  In these cases, a listener can be added to the operations history.  When an operation is added to the history, listeners can decide if their context should be added to the operation. 

 

Representing the contexts for an operation as a collection (empty graph) assumes that there is no inherent relationship between operation contexts.  This allows the most flexibility in defining contexts.   For example, an editor may perform an operation that affects both the editor’s document and the workspace at large.  These will be considered two independent contexts and assigned to the operation.  One could argue that the context for the document is simply a refinement of the workspace context, and that a hierarchical relationship between those contexts should exist.  Without more use cases, we avoid establishing any relationship between contexts at this time

 

Operation contexts that are used when manipulating well-known models should be accessible by API.  This can be done in multiple ways:

  1. API for obtaining a context can be added in the same place where the associated model can be obtained.  For example, an editor can provide both the document and the context for performing operations on the document as API.
  2. Contexts that are related directly to certain preexisting platform objects can be obtained using an adapter on that object.  For example, the workbench could add an operation context adapter factory for the workspace.  Clients could retrieve this adapter using context=workspace.getAdapter(IOperationContext.class);

 

The following interfaces represent the operation, the operation history, and the operation context.

 

public interface IOperation {

void addContext(IOperationContext);           // add the context to the operation’s contexts

boolean hasContext(IOperationContext);     //answer whether the receiver’s contexts contain the context

List getContexts();                                    // answer the list of contexts assigned to the receiver.

IStatus canExecute();                                // answer whether the receiver can be executed

IStatus canRedo();                                    // answer whether the receiver can be redone

IStatus canUndo();                                    // answer whether the receiver can be undone

void execute(IProgressMonitor);                  // execute the operation

void redo(IProgressMonitor);                       // redo the operation

void undo(IProgressMonitor);                      // undo the operation

void dispose();                                          // the operation is no longer needed

String getLabel();                                      // provide a label to name this operation for the user

String getDescription();                              // provide a description of this operation for the user

}

           

public interface IOperationHistory {

void add(IOperation);                                        // add the specified operation to the history

void redo(IOperationContext, IProgressMonitor);

            // perform the redo operation for the given context

void undo(IOperationContext, IProgressMonitor);

            // perform the undo operation appropriate for the given context.

IStatus canRedoIn(IOperationContext);              // answer whether redo is available in the given context

IStatus canUndoIn(IOperationContext);              // answer whether undo is available in the given context

void addOperationHistoryListener(IOperationHistoryListener);

            // add a listener for changes to the operation history

void removeOperationHistoryListener(IOperationHistoryListener);

            // remove the specified history listener from the receiver.

IOperation getRedoFor(IOperationContext);        // get the redo operation for the given context

IOperation [] getRedoHistoryFor(IOperationContext);  // answer the redo history list for the given context

IOperation getUndoFor(IOperationContext);        // answer the undo operation for the given context

IOperation [] getUndoHistoryFor(IOperationContext);  // answer the undo history list for the given context

void disposeAll(IOperationContext);

            // Remove all operations that have only the specified context.  Implementers must

            // determine how to handle operations that have additional contexts.

void setLimit(int);

            // set a limit on the operation history.  When the limit is reached, the oldest operation

            // will be disposed to make room for new operations.

}

 

public interface IOperationHistoryListener {

void operationAdded(IOperation);                      // an operation has been added to the history

void aboutToUndo(IOperation);                          // the operation is about to be undone

void undone(IOperation);                                  // the operation was undone

void aboutToRedo(IOperation);                          // the operation is about to be redone

void redone(IOperation);                                   // the operation was redone

}

 

public interface IOperationContext {

IOperationApproval getOperationApproval();

String getDescription();

String getLabel();

boolean equals(IOperationContext);

}

 

public interface IOperationApproval {

boolean proceedUndoing(IOperation);

boolean proceedRedoing(IOperation);

}

 

 

An operation is responsible for validating its ability to execute and undo against the current state of the application.  Validation may be requested even when an operation is not the most recently executed operation (or most recently undone operation).  This is important since operations in unrelated contexts may coexist in the same history unaware of one another’s existence. 

 

As operations are executed, they are added to the operation history.  Note that the operation history does not necessarily perform the initial execution, but rather assumes the operation is already executed before it is added to the history.  This is necessary for text editing operations, where the operations are happening as the user types and the recording/batching of operations for the history occur afterwards. 

 

The operation history is responsible for implementing the application’s undo model, and the decisions about handling multiple contexts.  If an application chooses to implement a strictly linear, global undo, then its implementation of IOperationHistory can ensure that requests to perform undo or redo in a particular context will fail if the most recent operation does not have that context.

 

More flexible undo models may be implemented with the assistance of the IOperationApproval that is assigned to an operation context.  This interface allows other contexts to be consulted when an operation is to be executed or undone.  The undo model defined by the operation history determines when to consult the IOperationApproval.  Possible implementations will be discussed in more detail in the workbench scenarios. 

Workbench Undo Semantics

The workbench will adopt a hybrid undo approach that allows selective undo across multiple contexts.  The workbench will allow undo/redo of any valid operation in the history, as long as there are no more recent operations in the history that share a context with the operation to be executed.   If the operation to be undone/redone has contexts that are also present in operations appearing later in the history, then the IOperationApproval for the contexts that have the conflicts will be consulted. 

 

A concrete example will help explain this. 

-          The user makes local edits in editor A.

-          The user initiates a refactoring operation whose context is “A” and “workspace.”

-          The user makes additional local edits to editor A.

-          The user goes to the navigator and selects Undo.

 

In the proposed implementation, the navigator would request an undo for the context “workspace.”  The refactoring operation triggered in the editor is the most recent operation that has the workspace context, but it also contains context “A.”  Since subsequent operations in the history also have context “A,”, the IOperationApproval for context “A” is consulted as to whether the undo should proceed.  The IOperationApproval  supplied by context “A” could do one of the following:

-          allow the undo to proceed, leaving the local changes to “A” in the undo history.

-          quietly undo the subsequent local changes to “A” and then allow the original undo to proceed.

-          switch to the editor and non-quietly undo the local changes to “A” before allowing the original undo to proceed.

-          prompt the user for the preferred action.

-          provide a user preference that drives the behavior of the scenario.

Workbench IDE undo contexts

 

To support the existing workbench undo scenarios, there will be two kinds of contexts defined in the workbench IDE. 

  1. The “workspace” context will be used to represent changes that affect resources in the workspace.  Refactoring operations will use this context.  Additional actions that affect the workspace (such as delete and create resources) would use this context.  Views that manipulate the workspace, such as the navigator and package explorer, will use this context when creating operations and querying for the current “undo operation.”  Views or editors that may be affected by workspace operations can add a listener to the operation history and add their context as needed to operations that have been added to the history.   The workspace context implementation is not defined at this time, but it should be accessible by API. 
  2. Editors will define a context that represents their particular editing session.   That is, the context will be unique per editor, but editors may in fact share the context implementation class.  Workspace-affecting operations, such as refactoring operations, that are triggered from an editor will be considered to occur in the context of both the editor and the workspace.  This allows refactoring operations to coexist with text editing operations in the editor’s operation history and also be accessible from the operation history of other views that support the workspace context, such as the navigator. The editor’s context can be added using the listener as described above.

 

Further definition of contexts and the supported operations are required:

-          The complete set of workspace operations (besides refactoring) that should be undoable must be defined.

-          Any additional contexts required by the SDK (Team?  Debug?) must be defined.

-          The operation contexts for the major views in the SDK must be defined.

-          The semantics for assigning multiple contexts must be defined.  For example, do refactoring operations that affect an open editor (but were triggered elsewhere) carry the context of that editor?  Should the editor decide this by listening for new operations that are added to the history?

 

The following are working assumptions about the undo operations:

-          There is no relationship between a perspective and an operation context, since switching perspectives will change the active part, and thus may or may not affect the current context.

-          Actions that only change the presentation in the workbench (open a view, switch perspectives, change the sort order) are not considered to be operations since they are  easily undone and redone through the same window mechanics.

 

Workbench UI for Undo/Redo

Minimal support

The Edit>Undo and Edit>Redo commands should be retargeted by most of the major views (list of views TBD).  If a view retargets Undo/Redo, then it knows what operation contexts it supports, and it uses these contexts to retrieve the available undo operation.

 

Edit>Undo is enabled based on the active part. 

            IStatus status = operationHistory.canUndoIn(myContext);

            // up to the active part to define behavior for anything between OK and FATAL.

 

The label for the undo action should be appended with a description of the operation.

            operationToUndo = operationHistory.getUndoFor(myContext);

            label = “Undo “+ operationToUndo.label();

 

Undo and Redo on the toolbar include a drop down that shows the operation history.  This history depends on the current part’s context.

            operationHistory.getUndoHistoryFor(myContext);

The drop down permits range selection of operations from the top down (see MS Word and other applications).  This UI implies a linear undo within any particular set of contexts.

 

The UI for handling special cases should be determined.  Consider the following scenarios:

 

Scenario #1:  Mixed contexts in an editor, undo from another view

-          The user makes local edits in editor A.

-          The user initiates a refactoring operation whose context is “A” and “workspace.”

-          The user makes additional local edits to editor A.

-          The user goes to the navigator and selects Undo.

 

The IOperationApproval for the editor’s context can implement UI for handling this case if desired.  Possible options include:

-          warn the user about the local changes in A and ask whether to undo them or leave them

-          provide a user preference that drives the behavior of the scenario.

 

The exact solution will be prototyped and may involve both a first-time prompt and subsequent preference (“Don’t ask me again”).

 

Scenario #2:  Editor undo triggers a non-local operation

-          The user makes local edits in editor A.

-          The user initiates a refactoring operation whose context is “A” and “workspace.”

-          The user makes additional local edits to editor A.

-          The user selects undo repeatedly from the editor

 

Most operations in the editor’s context affect only the editor.  The refactoring operation affects not only the editor, but other objects in the workspace.  It might surprise the user for the refactoring to be undone.  The editor could implement a warning dialog when it detects that the proposed undo operation has additional contexts. 

 

If the refactoring had been triggered elsewhere (say, editor “B” or the navigator), but resulted in changes to editor “A,” it is even more important that the user be warned about the operation invoked by the undo command. 

 

The  solution for this scenario will be prototyped and may involve both a warning and subsequent preference.  For example, a prompt could provide the label and description of the operation and warn that it affects other views.  The user could choose whether to proceed, and the choice could be remembered in a preference.

 

It appears that the proposed framework allows detection of these cases and hooks for supplying the necessary UI.

Advanced support

More selective undo approaches can be supported by the framework, but the ability to support them in the workbench depends upon the implementation of the individual operations and their ability to be undone independently.  The priority is TBD.  Ideas include:

 

Additional menu commands show the operation history.

Edit>Undo… shows the undo operation history based on the active part’s context.

Edit>Redo… shows the redo operation history based on the active part’s context.

            operationHistory.getUndoHistoryFor(myContext);

 

The operation history can be shown in a view or dialog.  Operations that are valid can be selected (one at a time) and undone (or redone) regardless of sequential order.  Multiple selections are not allowed since the validation state for an operation might change as other operations are undone.

 

The operation history view could allow changing of the filtering.  While the default filtering could depend on the active part’s context, the user could be shown the available contexts and choose one or more contexts (or none at all) that affect the filtering of the list. 

 

Note that the UI for the advanced cases can be implemented independently from the operations, and may be useful for testing the framework.

Compatibility Issues

JFace Text

JFace text currently has API for retrieving an IUndoManager from a text viewer.  The undo manager is connected to a text viewer and watches the changes that happen in the viewer.  As editing actions occurs, commands (TextCommand) are created and then “committed” to a command stack. 

 

TextCommand could be changed to implement IOperation.  The mapping from IOperation to TextCommand protocol is straightforward.  Currently the TextCommand directly pushes itself onto a local command stack of a viewer when a set of pending changes are committed.  Instead, it would need to set its context to one appropriate for the text viewer and add itself to the workbench operation history.   

 

IUndoManager API could be mapped as follows:

connect(ITextViewer)

Creates a context (myContext) appropriate for representing this document’s operations

disconnect(ITextViewer)

Performs necessary disconnect behavior and clears the operation history of operations involving the viewer.

operationHistory.disposeAll(myContext);

beginCompoundChange()

Creates a CompoundOperation and stores subsequent TextCommands in this operation.

endCompoundChange

Adds the current compound operation to the operation history.

reset()

Clears the operation history of operations involving the viewer.

            operationHistory.disposeAll(myContext);

setMaximumUndoLevel(int) – TBD

undoable()

Maps as follows:

status = operationHistory.canUndoIn(myContext);

return status.isOK();  // may require further checking of other cases

redoable()

Maps as follows:

status = operationHistory.canRedoIn(myContext);

return status.isOK();  // may require further checking of other cases

undo()

Use operationHistory.performUndoFor(myContext) instead of managing its own stack.

redo()

Use operationHistory.performRedoFor(myContext) instead of managing its own stack.

 

The TextCommand class is private, so no API mapping is discussed here.  However, its protocol is very similar to IOperation and the reimplementation/mapping is straightforward.

 

Issues: 

-          As discussed throughout this document, additional operations (such as compound operations or refactoring operations) might appear in the text viewer’s context, and therefore in its IUndoManager.  The implementation should be checked for any assumptions that the current operation is a TextCommand.

-          IUndoManager currently keeps a local history limit.  How do we handle this?

-          allow local history limits per context (seems complicated)

-          ignore this part of the API in the new world

-          decide how to affect the global history limit based on the local limit

Refactoring

Refactoring undo is currently implemented with change objects,  Change objects record workspace changes initiated by refactoring operations.  When a refactoring change is performed, it can optionally return another change that could be used to undo the change just executed.  The undo change is placed in refactoring’s IUndoManager.  Changes can be performed, but any undo or redo causes a new change to be created vs. having each change know how to undo and redo itself.

 

While this approach simplifies the protocol for an operation, it has not been used in the proposed framework for several reasons:

  1. It is not compatible with the implementation used by text, EMF, or GEF.
  2. It causes additional operations to be created before an undo is ever requested.  This violates the goal of “pay as you go.”

 

Additional protocol is provided for initializing data needed to do a live validation for execution.  Validation can be done with this cached information or on the fly.

 

Protocol is also provided to return the object modified by a change (getModifiedElement()).

 

The Change API could be mapped as follows.  Note that much of the protocol could be provided in an abstract “RefactoringOperation” or perhaps in a more generic “WorkspaceOperation.”  However we do not promote these concepts to the framework since different kinds of operations have different needs for caching model objects, validation state, etc. 

 

getModifiedElement()

getAdapter(Class)

Refactoring changes need to know which model element is modified by the change.  They must also implement IAdaptable.  This protocol can be provided in an abstract RefactoringOperation and interpreted by individual subclasses as done today.  This protocol might in fact be suitable for all workspace-affecting operations. 

getParent()

setParent(Change)

isEnabled()

setEnabled(boolean)

The associated fields and protocol can be defined in an abstract class.

initializeValidationData(IProgressMonitor)

This hook allows changes to cache local validation data, or to hook up listeners to the model and listen for changes that may invalidate the change.  This protocol could be provided in the abstract refactoring class.

dispose()

Maps directly.

getName()

Maps to operation.getLabel

isValid(IProgressMonitor)

Maps to canUndo(), canRedo(), and canExecute().  See discussion below.

perform(IProgressMonitor)

Maps to undo(IProgressMonitor), redo(IProgressMonitor) and canExecute(IProgressMonitor).  See discussion below.

 

Issues

-          Refactoring change objects do not support direct undo() or redo().  Rather, a change that supports undo() returns a new change that “undoes” it whenever it is executed.  The code for refactoring changes would have to be refactored in one of these ways:

o        Eliminate the creation of the undo change and move this work into an undo() method.

o        Continue to create the undo change when performing a change, but instead of returning it, cache it in the change itself.  Undo protocol checks for the presence of this change and uses it for any undo validation or execution requests.

-          Validation of refactoring changes is assumed to be expensive, so a progress monitor is provided in validation protocol.  This concept is not promoted to the framework since validity is checked when building the undo menu command.  Is the progress monitor absolutely necessary for validation vs. execution?

 

It is likely that many views and editors in the workbench IDE will be aware of this special kind of IOperation.  An additional interface, IWorkspaceOperation, may evolve as refactoring changes are adapted to the new operations framework.  Patterns surrounding the workspace operation will develop, particularly involving operation contexts.  For example, views and editors (such as text editors) that are workspace-aware could listen to the operations history.  As new workspace operations are added to the history, the modified element can be queried by the listener.  The listener can add its operation context to the operation if desired. 

 

GEF

GEF supplies a Command framework and a CommandStack which manages an internal undo and redo stack.  Commands are pushed onto the stack when they are executed by the stack.  Most GEF command protocol can be mapped to the proposed operation protocol.  The GEF commands are very similar to IOperation without the notion of contexts. 

 

Issues for GEF arise in the differences between GEF’s command stack and the proposed operation history.  We assume that GEF intends for GEF commands to coexist with other workbench operations in a shared undo history.  If so, then the command stack must be replaced by the operation history.  Issues include:

 

isDirty()

markSaveLocation()

GEF supports a marker (saveLocation) that becomes dirty if any commands are added after the mark is made.  If this concept is still needed in light of the new framework, then GEF might have to mark the location inside its own commands or using a dummy marker command.

execute(Command)

The GEF command stack executes commands on behalf of the client and adds them to the stack, while the operation history does not do the execution on behalf of the client.  GEF clients that create GEF commands would alter the pattern “commandStack.execute(someCommand)” to instead use “someCommand.execute(null); operationHistory.add(someCommand)” 

addCommandStackListener(CommandStackListener)

GEF is evolving its CommandListener interface in R3.1 to include additional notification.  This must be reconciled with the proposed support in IOperationHistoryListener and the ability to further validate commands using IOperationApproval. 

EMF/WTP

The EMF command framework is very similar to the GEF support and has similar issues to those described above.  Additional issues arise for:

 

getResult();

This method answers the result of command execution when the command is in certain states.  Since clients can execute an operation explicitly in the framework, EMF code could query the result after execution and store it in an EMF abstract operation.  This protocol could then be provided in the EMF operations.

getAffectedObjects();

This method is used to highlight the appropriate objects in a view to show what the EMF command did.    It is not clear that this will be generally necessary.  This protocol could be added in an EMF operation abstract class.

 

As discussed with refactoring changes, an abstract class providing the mapped protocol will likely evolve, along with usage patterns for listening for new EMF operations and assigning appropriate operation contexts.


Appendix A:  Workbench Undo Scenarios

The following scenarios were used while discussing possible designs for a common undo framework.   Most of these scenarios involve conflicting contexts or scopes.  These scenarios, involving mixed text edits and refactoring operations, have complicated previous efforts to define a common undo framework. 

 

The scenarios assume that an integrated undo in the workbench should present a common “Undo” command rather than the separate commands employed today, and that the “Undo” command is descriptive enough to imply the operation that will take place.

 

For each scenario described, we attempt to describe the desired outcome, the outcome in R3.0.1 where applicable, and any interesting alternative outcomes.

 

Note that some scenarios involve nearly the same user interactions, but are different in desired outcome based on what the user was trying to accomplish while performing the operations.  Specific user intent is described in order to clarify the scenario.

 

Scenario #1:  User is editing source file “A.”  While editing the document, some interrupt occurs causing the user to go to the navigator and refactor some code unrelated to A.  When the refactoring is complete, the user returns to the editor on “A” and notices a typo.  The user selects Edit>Undo.

           

Desired Outcome:  The last text editing operation made before the unrelated refactoring operation is undone.  The typo is corrected.  The undo command was described as “undo typing”

 

R3.0.1 Outcome:  Desired Outcome.

 

Proposal Outcome:  Desired Outcome.  (Unrelated refactoring operation did not have the editor’s context). 

 

Scenario #2:  User is editing source file “A.”  While editing the document, some interrupt occurs causing the user to go to the navigator and refactor some code.  The refactoring operation updates references to the refactored code in file “A.”  When the refactoring is complete, the user returns to the editor on “A” and notices a typo.  The user selects Edit>Undo.

           

Desired Outcome:  This scenario depends largely on the user’s intent.  If the typo was in a location completely unrelated to the refactor, the user expects the last text editing operation made before the refactoring operation to be undone, correcting the typo. 

 

R3.0.1 Outcome:  The text edit caused by the refactoring operation is undone, but the refactoring itself is not undone.  The typo remains until the user invokes subsequent “Undo.”

 

Proposal Outcome:  This outcome can be implemented in different ways.  If the refactoring operation command is assigned the context for “A,” since A was affected, then the refactoring operation would be undone before the typo.  However, the editor could notice that the operation has multiple contexts and prompt the user with an operation history.  

 

Scenario #3:  User is editing source file “A.”  While editing the document, some interrupt occurs causing the user to go to the navigator and refactor some code.  The refactoring operation updates references to the refactored code in file “A.”  When the refactoring is complete, the user recalls making a typo just before the refactor.  The user returns to the editor on “A” and looks for the typo.  It is not there, as it has been replaced with new code due to the refactoring operation. The user selects Edit>Undo.

           

Desired Outcome:  At this point, the user needs information that helps explain the disappearance of the typo.  In this case, there are two helpful options:

  1. The last performed refactoring operation is undone.  This returns the user to the typo, in effect showing why the typo had disappeared.  The user then selects “Redo,” having been satisfied that the typo was corrected by the refactoring operation.  This operation is somewhat “heavyweight,” but one could argue that it’s the more expected outcome.
  2. The text edit caused by the refactoring is undone, exposing the typo and, in effect, showing why it’s gone.  The user then selects “Redo,” having been satisfied that the typo was already corrected by the refactoring operation.  This is a “lighter weight” solution but perhaps more confusing.

 

R3.0.1 Outcome:  Desired Outcome #2.

 

Proposal outcome:  Desired Outcome #1.  This scenario is the same as scenario #2 except for user intent, so the UI could present similar choices as described above. 

 

Scenario #4:  User is editing source file “A” and considers renaming a method in “A.”  While typing a new name for the method, the user realizes that a refactoring operation will simplify the editing process.  The user chooses “Refactor>Rename” to rename the method.  The refactor causes references to be updated in other files.  The user then decides that the rename was not appropriate.  The user selects Edit>Undo.

           

Desired Outcome:  The refactoring operation is undone, updating all of the references. The user is returned to the typing state before the refactor.  Selecting another Edit>Undo will undo the typing of the new name and return to the old name.

 

R3.0.1 Outcome:  The text edit implied by the refactoring operation is undone, and the user is returned to the typing state before the refactor.  However, the refactoring itself is not undone.  This is undoable from a separate Refactor>Undo.

 

Proposal outcome:  Desired Outcome.  A user prompt or verification may occur as discussed in other scenarios.

 

Scenario #5:  User is editing source file “A” and considers renaming a method in “A.”  The user decides that a refactoring operation is in order.  The user chooses “Refactor>Rename” to rename the method.  The refactor causes references to be updated in other files.  The user then makes additional edits to the javadoc for the renamed method and realizes that the rename was not appropriate.  The user selects Edit>Undo.

           

Desired Outcome:  The most recent editing changes to the javadoc are undone.  The user can continue selecting “Undo” until the refactoring operation is undone, updating all of the references.

 

R3.0.1 Outcome:  The most recent editing changes to the javadoc are undone.  The user can continue selecting “Undo” until the text edit implied by the refactoring operation is undone.  However, the refactoring itself is not undone.  This is undoable from a separate Refactor>Undo.

 

Proposal outcome:  Desired Outcome.

 

Scenario #6:  User is editing source file “A” and considers renaming a method in “A.”  The user decides that a refactoring operation is in order.  The user chooses “Refactor>Rename” to rename the method.  The refactor causes references to be updated in other files.  The user then proceeds to edit javadoc in another part of the file, unrelated to change.  The user realizes the refactor was not appropriate, and not wanting to undo the edits to the javadoc, selects the navigator view, and selects Edit>Undo, assuming that the navigator undo won’t affect local edit changes.

           

Desired Outcome:  The refactoring operation is undone, but the changes to the javadoc made afterward are not undone.  The local changes to file “A” could only be undone while inside editor A.

 

R3.0.1 Outcome:  The refactoring operation is undone, and the most recent changes to the file are lost.

 

Proposal outcome:  This can be implemented in different ways with IOperationApproval.  The operation history would notice that the chosen operation has context “A” and that later changes for “A” are in the history.  The IOperationApproval assigned to context “A” could prompt the user as to whether to undo the more recent changes in “A.”

 



[1] Literature addressing multi-user undo [Abowd & Dix] addresses similar scoping issues in a multi-user model.  Collaborating users may expect a global undo scope while users working in separate contexts may not.  Users may be moving between collaboration and separation in a single work session, and the system cannot detect which is the case.  Undo implementations in these systems typically keep track of the user who initiated the operation within a global history and filter the history appropriately.  Potential conflicts are brought to the attention of the user(s).