/* * Flush the undo stack of all operations that have the given context. */ private void flushUndo(IUndoContext context) { if (DEBUG_OPERATION_HISTORY_DISPOSE) { Tracing.printTrace( "OPERATIONHISTORY", "Flushing undo history for " + context); // $NON-NLS-1$ //$NON-NLS-2$ } synchronized (undoRedoHistoryLock) { // Get all operations that have the context (or one that matches) Object[] filtered = filter(undoList, context); for (int i = 0; i < filtered.length; i++) { IUndoableOperation operation = (IUndoableOperation) filtered[i]; if (context == GLOBAL_UNDO_CONTEXT || operation.getContexts().length == 1) { // remove the operation if it only has the context or we are // flushing all undoList.remove(operation); internalRemove(operation); } else { // remove the reference to the context. // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=161786 // It is not enough to simply remove the context. There could // be one or more contexts that match the one we are trying to // dispose. IUndoContext[] contexts = operation.getContexts(); for (int j = 0; j < contexts.length; j++) { if (contexts[j].matches(context)) { operation.removeContext(contexts[j]); } } if (operation.getContexts().length == 0) { undoList.remove(operation); internalRemove(operation); } } } } /* * There may be an open composite. If it has this context, then the * context must be removed. If it has only this context or we are * flushing all operations, then null it out and notify that we are * ending it. We don't remove it since it was never added. */ ICompositeOperation endedComposite = null; synchronized (openCompositeLock) { if (openComposite != null) { if (openComposite.hasContext(context)) { if (context == GLOBAL_UNDO_CONTEXT || openComposite.getContexts().length == 1) { endedComposite = openComposite; openComposite = null; } else { openComposite.removeContext(context); } } } } // notify outside of the synchronized block. if (endedComposite != null) { notifyNotOK(endedComposite); } }
/* * Force the undo history for the given context to contain max or less * items. */ private void forceUndoLimit(IUndoContext context, int max) { synchronized (undoRedoHistoryLock) { Object[] filtered = filter(undoList, context); int size = filtered.length; if (size > 0) { int index = 0; while (size > max) { IUndoableOperation removed = (IUndoableOperation) filtered[index]; if (context == GLOBAL_UNDO_CONTEXT || removed.getContexts().length == 1) { /* * remove the operation if we are enforcing a global limit * or if the operation only has the specified context */ undoList.remove(removed); internalRemove(removed); } else { /* * if the operation has multiple contexts and we've reached * the limit for only one of them, then just remove the * context, not the operation. */ removed.removeContext(context); } size--; index++; } } } }
@Override public void add(IUndoableOperation operation) { Assert.isNotNull(operation); /* * If we are in the middle of executing an open batching operation, and * this is not that operation, then we need only add the context of the * new operation to the batch. The operation itself is disposed since we * will never undo or redo it. We consider it to be triggered by the * batching operation and assume that its undo will be triggered by the * batching operation undo. */ synchronized (openCompositeLock) { if (openComposite != null && openComposite != operation) { openComposite.add(operation); return; } } if (checkUndoLimit(operation)) { synchronized (undoRedoHistoryLock) { undoList.add(operation); } notifyAdd(operation); // flush redo stack for related contexts IUndoContext[] contexts = operation.getContexts(); for (int i = 0; i < contexts.length; i++) { flushRedo(contexts[i]); } } else { // Dispose the operation since we will not have a reference to it. operation.dispose(); } }
/* * Flush the redo stack of all operations that have the given context. */ private void flushRedo(IUndoContext context) { if (DEBUG_OPERATION_HISTORY_DISPOSE) { Tracing.printTrace( "OPERATIONHISTORY", "Flushing redo history for " + context); // $NON-NLS-1$ //$NON-NLS-2$ } synchronized (undoRedoHistoryLock) { Object[] filtered = filter(redoList, context); for (int i = 0; i < filtered.length; i++) { IUndoableOperation operation = (IUndoableOperation) filtered[i]; if (context == GLOBAL_UNDO_CONTEXT || operation.getContexts().length == 1) { // remove the operation if it only has the context or we are // flushing all redoList.remove(operation); internalRemove(operation); } else { // remove the reference to the context. // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=161786 // It is not enough to simply remove the context. There could // be one or more contexts that match the one we are trying to // dispose. IUndoContext[] contexts = operation.getContexts(); for (int j = 0; j < contexts.length; j++) { if (contexts[j].matches(context)) { operation.removeContext(contexts[j]); } } if (operation.getContexts().length == 0) { redoList.remove(operation); internalRemove(operation); } } } } }
/** * Perform the redo. All validity checks have already occurred. * * @param monitor * @param operation */ private IStatus doRedo(IProgressMonitor monitor, IAdaptable info, IUndoableOperation operation) throws ExecutionException { IStatus status = getRedoApproval(operation, info); if (status.isOK()) { notifyAboutToRedo(operation); try { status = operation.redo(monitor, info); } catch (OperationCanceledException e) { status = Status.CANCEL_STATUS; } catch (ExecutionException e) { notifyNotOK(operation); if (DEBUG_OPERATION_HISTORY_UNEXPECTED) { Tracing.printTrace( "OPERATIONHISTORY", //$NON-NLS-1$ "ExecutionException while redoing " + operation); // $NON-NLS-1$ } throw e; } catch (Exception e) { notifyNotOK(operation); if (DEBUG_OPERATION_HISTORY_UNEXPECTED) { Tracing.printTrace( "OPERATIONHISTORY", //$NON-NLS-1$ "Exception while redoing " + operation); // $NON-NLS-1$ } throw new ExecutionException( "While redoing the operation, an exception occurred", e); // $NON-NLS-1$ } } // if successful, the operation is removed from the redo history and // placed back in the undo history. if (status.isOK()) { boolean addedToUndo = true; synchronized (undoRedoHistoryLock) { redoList.remove(operation); if (checkUndoLimit(operation)) { undoList.add(operation); } else { addedToUndo = false; } } // dispose the operation since we could not add it to the // stack and will no longer have a reference to it. if (!addedToUndo) { operation.dispose(); } // notify listeners must happen after history is updated notifyRedone(operation); } else { notifyNotOK(operation, status); } return status; }
@Override public IUndoableOperation getUndoOperation(IUndoContext context) { Assert.isNotNull(context); synchronized (undoRedoHistoryLock) { for (int i = undoList.size() - 1; i >= 0; i--) { IUndoableOperation operation = undoList.get(i); if (operation.hasContext(context)) { return operation; } } } return null; }
/** * Check the undo limit before adding an operation. Return a boolean indicating whether the undo * should proceed. */ private boolean checkUndoLimit(IUndoableOperation operation) { IUndoContext[] contexts = operation.getContexts(); for (int i = 0; i < contexts.length; i++) { int limit = getLimit(contexts[i]); if (limit > 0) { forceUndoLimit(contexts[i], limit - 1); } else { // this context has a 0 limit operation.removeContext(contexts[i]); } } return operation.getContexts().length > 0; }
/* * (non-Javadoc) * * @see org.eclipse.core.commands.operations.IOperationApprover#proceedRedoing(org.eclipse.core.commands.operations.IUndoableOperation, * org.eclipse.core.commands.operations.IOperationHistory, * org.eclipse.core.runtime.IAdaptable) */ public final IStatus proceedRedoing( IUndoableOperation operation, IOperationHistory history, IAdaptable info) { IUndoContext[] contexts = operation.getContexts(); for (int i = 0; i < contexts.length; i++) { IUndoContext context = contexts[i]; if (history.getRedoOperation(context) != operation) { IStatus status = allowLinearRedoViolation(operation, context, history, info); if (!status.isOK()) return status; } } return Status.OK_STATUS; }
/* * Filter the specified list to include only the specified undo context. */ private IUndoableOperation[] filter(List<IUndoableOperation> list, IUndoContext context) { /* * This method is used whenever there is a need to filter the undo or * redo history on a particular context. Currently there are no caches * kept to optimize repeated requests for the same filter. If benchmarks * show this to be a common pattern that causes performances problems, * we could implement a filtered cache here that is nullified whenever * the global history changes. */ List<IUndoableOperation> filtered = new ArrayList<>(); synchronized (undoRedoHistoryLock) { Iterator<IUndoableOperation> iterator = list.iterator(); while (iterator.hasNext()) { IUndoableOperation operation = iterator.next(); if (operation.hasContext(context)) { filtered.add(operation); } } } return filtered.toArray(new IUndoableOperation[filtered.size()]); }
@Override public IStatus undo(IUndoContext context, IProgressMonitor monitor, IAdaptable info) throws ExecutionException { Assert.isNotNull(context); IUndoableOperation operation = getUndoOperation(context); // info if there is no operation if (operation == null) { return IOperationHistory.NOTHING_TO_UNDO_STATUS; } // error if operation is invalid if (!operation.canUndo()) { if (DEBUG_OPERATION_HISTORY_UNEXPECTED) { Tracing.printTrace( "OPERATIONHISTORY", "Undo operation not valid - " + operation); // $NON-NLS-1$ //$NON-NLS-2$ } return IOperationHistory.OPERATION_INVALID_STATUS; } return doUndo(monitor, info, operation); }
@Override public IStatus undoOperation( IUndoableOperation operation, IProgressMonitor monitor, IAdaptable info) throws ExecutionException { Assert.isNotNull(operation); IStatus status; if (operation.canUndo()) { status = doUndo(monitor, info, operation); } else { if (DEBUG_OPERATION_HISTORY_UNEXPECTED) { Tracing.printTrace( "OPERATIONHISTORY", "Undo operation not valid - " + operation); // $NON-NLS-1$//$NON-NLS-2$ } status = IOperationHistory.OPERATION_INVALID_STATUS; } return status; }
/* * Remove the operation by disposing it and notifying listeners. */ private void internalRemove(IUndoableOperation operation) { operation.dispose(); notifyRemoved(operation); }
@Override public IStatus execute(IUndoableOperation operation, IProgressMonitor monitor, IAdaptable info) throws ExecutionException { Assert.isNotNull(operation); // error if operation is invalid if (!operation.canExecute()) { return IOperationHistory.OPERATION_INVALID_STATUS; } // check with the operation approvers IStatus status = getExecuteApproval(operation, info); if (!status.isOK()) { // not approved. No notifications are sent, just return the status. return status; } /* * If we are in the middle of an open composite, then we will add this * operation to the open operation rather than add the operation to the * history. We will still execute it. */ boolean merging = false; synchronized (openCompositeLock) { if (openComposite != null) { // the composite shouldn't be executed explicitly while it is // still // open if (openComposite == operation) { return IOperationHistory.OPERATION_INVALID_STATUS; } openComposite.add(operation); merging = true; } } /* * Execute the operation */ if (!merging) { notifyAboutToExecute(operation); } try { status = operation.execute(monitor, info); } catch (OperationCanceledException e) { status = Status.CANCEL_STATUS; } catch (ExecutionException e) { notifyNotOK(operation); throw e; } catch (Exception e) { notifyNotOK(operation); throw new ExecutionException( "While executing the operation, an exception occurred", e); // $NON-NLS-1$ } // if successful, the notify listeners are notified and the operation is // added to the history if (!merging) { if (status.isOK()) { notifyDone(operation); add(operation); } else { notifyNotOK(operation, status); // dispose the operation since we did not add it to the stack // and will no longer have a reference to it. operation.dispose(); } } // all other severities are not interpreted. Simply return the status. return status; }
@Override public boolean canUndo(IUndoContext context) { // null context is allowed and passed through IUndoableOperation operation = getUndoOperation(context); return (operation != null && operation.canUndo()); }