Copyright (c) 2006, 2019 IBM Corporation and others. This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which accompanies this distribution, and is available at https://www.eclipse.org/legal/epl-2.0/ SPDX-License-Identifier: EPL-2.0 Contributors: IBM Corporation - initial API and implementation Paul Pazderski - Bug 549755: use DocumentRewriteSession if operation has lot of changes
/******************************************************************************* * Copyright (c) 2006, 2019 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * Contributors: * IBM Corporation - initial API and implementation * Paul Pazderski - Bug 549755: use {@link DocumentRewriteSession} if operation has lot of changes *******************************************************************************/
package org.eclipse.text.undo; import java.util.ArrayList; import java.util.List; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.commands.operations.AbstractOperation; import org.eclipse.core.commands.operations.IContextReplacingOperation; import org.eclipse.core.commands.operations.IOperationHistory; import org.eclipse.core.commands.operations.IOperationHistoryListener; import org.eclipse.core.commands.operations.IUndoContext; import org.eclipse.core.commands.operations.IUndoableOperation; import org.eclipse.core.commands.operations.ObjectUndoContext; import org.eclipse.core.commands.operations.OperationHistoryEvent; import org.eclipse.core.commands.operations.OperationHistoryFactory; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.Status; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.DocumentRewriteSession; import org.eclipse.jface.text.DocumentRewriteSessionType; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentExtension4; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.TextUtilities;
A standard implementation of a document-based undo manager that creates an undo history based on changes to its document.

Based on the 3.1 implementation of DefaultUndoManager, it was implemented using the document-related manipulations defined in the original DefaultUndoManager, by separating the document manipulations from the viewer-specific processing.

The classes representing individual text edits (formerly text commands) were promoted from inner types to their own classes in order to support reassignment to a different undo manager.

This class is not intended to be subclassed.

See Also:
Since:3.2
@noextendThis class is not intended to be subclassed by clients.
/** * A standard implementation of a document-based undo manager that * creates an undo history based on changes to its document. * <p> * Based on the 3.1 implementation of DefaultUndoManager, it was implemented * using the document-related manipulations defined in the original * DefaultUndoManager, by separating the document manipulations from the * viewer-specific processing.</p> * <p> * The classes representing individual text edits (formerly text commands) * were promoted from inner types to their own classes in order to support * reassignment to a different undo manager.</p> * <p> * This class is not intended to be subclassed. * </p> * * @see IDocumentUndoManager * @see DocumentUndoManagerRegistry * @see IDocumentUndoListener * @see org.eclipse.jface.text.IDocument * @since 3.2 * @noextend This class is not intended to be subclassed by clients. */
public class DocumentUndoManager implements IDocumentUndoManager {
Represents an undo-able text change, described as the replacement of some preserved text with new text.

Based on the DefaultUndoManager.TextCommand from R3.1.

/** * Represents an undo-able text change, described as the * replacement of some preserved text with new text. * <p> * Based on the DefaultUndoManager.TextCommand from R3.1. * </p> */
private static class UndoableTextChange extends AbstractOperation {
The start index of the replaced text.
/** The start index of the replaced text. */
protected int fStart= -1;
The end index of the replaced text.
/** The end index of the replaced text. */
protected int fEnd= -1;
The newly inserted text.
/** The newly inserted text. */
protected String fText;
The replaced text.
/** The replaced text. */
protected String fPreservedText;
The undo modification stamp.
/** The undo modification stamp. */
protected long fUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
The redo modification stamp.
/** The redo modification stamp. */
protected long fRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
The undo manager that generated the change.
/** The undo manager that generated the change. */
protected DocumentUndoManager fDocumentUndoManager;
Creates a new text change.
Params:
  • manager – the undo manager for this change
/** * Creates a new text change. * * @param manager the undo manager for this change */
UndoableTextChange(DocumentUndoManager manager) { super(UndoMessages.getString("DocumentUndoManager.operationLabel")); //$NON-NLS-1$ this.fDocumentUndoManager= manager; addContext(manager.getUndoContext()); }
Re-initializes this text change.
/** * Re-initializes this text change. */
protected void reinitialize() { fStart= fEnd= -1; fText= fPreservedText= null; fUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; fRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; }
Sets the start and the end index of this change.
Params:
  • start – the start index
  • end – the end index
/** * Sets the start and the end index of this change. * * @param start the start index * @param end the end index */
protected void set(int start, int end) { fStart= start; fEnd= end; fText= null; fPreservedText= null; } @Override public void dispose() { reinitialize(); }
Undo the change described by this change.
/** * Undo the change described by this change. */
protected void undoTextChange() { try { if (fDocumentUndoManager.fDocument instanceof IDocumentExtension4) { ((IDocumentExtension4) fDocumentUndoManager.fDocument).replace(fStart, fText .length(), fPreservedText, fUndoModificationStamp); } else { fDocumentUndoManager.fDocument.replace(fStart, fText.length(), fPreservedText); } } catch (BadLocationException x) { } } @Override public boolean canUndo() { if (isValid()) { if (fDocumentUndoManager.fDocument instanceof IDocumentExtension4) { long docStamp= ((IDocumentExtension4) fDocumentUndoManager.fDocument) .getModificationStamp(); // Normal case: an undo is valid if its redo will restore // document to its current modification stamp boolean canUndo= docStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP || docStamp >= getRedoModificationStamp(); /* * Special case to check if the answer is false. If the last * document change was empty, then the document's modification * stamp was incremented but nothing was committed. The * operation being queried has an older stamp. In this case * only, the comparison is different. A sequence of document * changes that include an empty change is handled correctly * when a valid commit follows the empty change, but when * #canUndo() is queried just after an empty change, we must * special case the check. The check is very specific to prevent * false positives. see * https://bugs.eclipse.org/bugs/show_bug.cgi?id=98245 */ if (!canUndo && this == fDocumentUndoManager.fHistory .getUndoOperation(fDocumentUndoManager.fUndoContext) // this is the latest operation && this != fDocumentUndoManager.fCurrent // there is a more current operation not on the stack && !fDocumentUndoManager.fCurrent.isValid() // the current operation is not a valid document // modification && fDocumentUndoManager.fCurrent.fUndoModificationStamp != // the invalid current operation has a document stamp IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) { canUndo= fDocumentUndoManager.fCurrent.fRedoModificationStamp == docStamp; } /* * When the composite is the current operation, it may hold the * timestamp of a no-op change. We check this here rather than * in an override of canUndo() in UndoableCompoundTextChange simply to * keep all the special case checks in one place. */ if (!canUndo && this == fDocumentUndoManager.fHistory .getUndoOperation(fDocumentUndoManager.fUndoContext) && // this is the latest operation this instanceof UndoableCompoundTextChange && this == fDocumentUndoManager.fCurrent && // this is the current operation this.fStart == -1 && // the current operation text is not valid fDocumentUndoManager.fCurrent.fRedoModificationStamp != IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) { // but it has a redo stamp canUndo= fDocumentUndoManager.fCurrent.fRedoModificationStamp == docStamp; } return canUndo; } // if there is no timestamp to check, simply return true per the // 3.0.1 behavior return true; } return false; } @Override public boolean canRedo() { if (isValid()) { if (fDocumentUndoManager.fDocument instanceof IDocumentExtension4) { long docStamp= ((IDocumentExtension4) fDocumentUndoManager.fDocument) .getModificationStamp(); return docStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP || docStamp == getUndoModificationStamp(); } // if there is no timestamp to check, simply return true per the // 3.0.1 behavior return true; } return false; } @Override public boolean canExecute() { return fDocumentUndoManager.isConnected(); } @Override public IStatus execute(IProgressMonitor monitor, IAdaptable uiInfo) { // Text changes execute as they are typed, so executing one has no // effect. return Status.OK_STATUS; }
{@inheritDoc} Notifies clients about the undo.
/** * {@inheritDoc} * Notifies clients about the undo. */
@Override public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) { if (isValid()) { fDocumentUndoManager.fireDocumentUndo(fStart, fPreservedText, fText, uiInfo, DocumentUndoEvent.ABOUT_TO_UNDO, false); undoTextChange(); fDocumentUndoManager.resetProcessChangeState(); fDocumentUndoManager.fireDocumentUndo(fStart, fPreservedText, fText, uiInfo, DocumentUndoEvent.UNDONE, false); return Status.OK_STATUS; } return IOperationHistory.OPERATION_INVALID_STATUS; }
Re-applies the change described by this change.
/** * Re-applies the change described by this change. */
protected void redoTextChange() { try { if (fDocumentUndoManager.fDocument instanceof IDocumentExtension4) { ((IDocumentExtension4) fDocumentUndoManager.fDocument).replace(fStart, fEnd - fStart, fText, fRedoModificationStamp); } else { fDocumentUndoManager.fDocument.replace(fStart, fEnd - fStart, fText); } } catch (BadLocationException x) { } }
Re-applies the change described by this change that was previously undone. Also notifies clients about the redo.
Params:
  • monitor – the progress monitor to use if necessary
  • uiInfo – an adaptable that can provide UI info if needed
Returns:the status
/** * Re-applies the change described by this change that was previously * undone. Also notifies clients about the redo. * * @param monitor the progress monitor to use if necessary * @param uiInfo an adaptable that can provide UI info if needed * @return the status */
@Override public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) { if (isValid()) { fDocumentUndoManager.fireDocumentUndo(fStart, fText, fPreservedText, uiInfo, DocumentUndoEvent.ABOUT_TO_REDO, false); redoTextChange(); fDocumentUndoManager.resetProcessChangeState(); fDocumentUndoManager.fireDocumentUndo(fStart, fText, fPreservedText, uiInfo, DocumentUndoEvent.REDONE, false); return Status.OK_STATUS; } return IOperationHistory.OPERATION_INVALID_STATUS; }
Update the change in response to a commit.
/** * Update the change in response to a commit. */
protected void updateTextChange() { fText= fDocumentUndoManager.fTextBuffer.toString(); fDocumentUndoManager.fTextBuffer.setLength(0); fPreservedText= fDocumentUndoManager.fPreservedTextBuffer.toString(); fDocumentUndoManager.fPreservedTextBuffer.setLength(0); }
Creates a new uncommitted text change depending on whether a compound change is currently being executed.
Returns:a new, uncommitted text change or a compound text change
/** * Creates a new uncommitted text change depending on whether a compound * change is currently being executed. * * @return a new, uncommitted text change or a compound text change */
protected UndoableTextChange createCurrent() { if (fDocumentUndoManager.fFoldingIntoCompoundChange) { return new UndoableCompoundTextChange(fDocumentUndoManager); } return new UndoableTextChange(fDocumentUndoManager); }
Commits the current change into this one.
/** * Commits the current change into this one. */
protected void commit() { if (fStart < 0) { if (fDocumentUndoManager.fFoldingIntoCompoundChange) { fDocumentUndoManager.fCurrent= createCurrent(); } else { reinitialize(); } } else { updateTextChange(); fDocumentUndoManager.fCurrent= createCurrent(); } fDocumentUndoManager.resetProcessChangeState(); }
Updates the text from the buffers without resetting the buffers or adding anything to the stack.
/** * Updates the text from the buffers without resetting the buffers or adding * anything to the stack. */
protected void pretendCommit() { if (fStart > -1) { fText= fDocumentUndoManager.fTextBuffer.toString(); fPreservedText= fDocumentUndoManager.fPreservedTextBuffer.toString(); } }
Attempt a commit of this change and answer true if a new fCurrent was created as a result of the commit.
Returns:true if the change was committed and created a new fCurrent, false if not
/** * Attempt a commit of this change and answer true if a new fCurrent was * created as a result of the commit. * * @return <code>true</code> if the change was committed and created * a new <code>fCurrent</code>, <code>false</code> if not */
protected boolean attemptCommit() { pretendCommit(); if (isValid()) { fDocumentUndoManager.commit(); return true; } return false; }
Checks whether this text change is valid for undo or redo.
Returns:true if the change is valid for undo or redo
/** * Checks whether this text change is valid for undo or redo. * * @return <code>true</code> if the change is valid for undo or redo */
protected boolean isValid() { return fStart > -1 && fEnd > -1 && fText != null; } @Override public String toString() { String delimiter= ", "; //$NON-NLS-1$ StringBuilder text= new StringBuilder(super.toString()); text.append("\n"); //$NON-NLS-1$ text.append(this.getClass().getName()); text.append(" undo modification stamp: "); //$NON-NLS-1$ text.append(fUndoModificationStamp); text.append(" redo modification stamp: "); //$NON-NLS-1$ text.append(fRedoModificationStamp); text.append(" start: "); //$NON-NLS-1$ text.append(fStart); text.append(delimiter); text.append("end: "); //$NON-NLS-1$ text.append(fEnd); text.append(delimiter); text.append("text: '"); //$NON-NLS-1$ text.append(fText); text.append('\''); text.append(delimiter); text.append("preservedText: '"); //$NON-NLS-1$ text.append(fPreservedText); text.append('\''); return text.toString(); }
Return the undo modification stamp
Returns:the undo modification stamp for this change
/** * Return the undo modification stamp * * @return the undo modification stamp for this change */
protected long getUndoModificationStamp() { return fUndoModificationStamp; }
Return the redo modification stamp
Returns:the redo modification stamp for this change
/** * Return the redo modification stamp * * @return the redo modification stamp for this change */
protected long getRedoModificationStamp() { return fRedoModificationStamp; } }
Represents an undo-able text change consisting of several individual changes.
/** * Represents an undo-able text change consisting of several individual * changes. */
private static class UndoableCompoundTextChange extends UndoableTextChange {
The list of individual changes
/** The list of individual changes */
private List<UndoableTextChange> fChanges= new ArrayList<>();
Creates a new compound text change.
Params:
  • manager – the undo manager for this change
/** * Creates a new compound text change. * * @param manager the undo manager for this change */
UndoableCompoundTextChange(DocumentUndoManager manager) { super(manager); }
Adds a new individual change to this compound change.
Params:
  • change – the change to be added
/** * Adds a new individual change to this compound change. * * @param change the change to be added */
protected void add(UndoableTextChange change) { fChanges.add(change); } @Override public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) { int size= fChanges.size(); if (size > 0) { UndoableTextChange c; c= fChanges.get(0); fDocumentUndoManager.fireDocumentUndo(c.fStart, c.fPreservedText, c.fText, uiInfo, DocumentUndoEvent.ABOUT_TO_UNDO, size > 1); DocumentRewriteSession rewriteSession= null; if (size > 25 && fDocumentUndoManager.fDocument instanceof IDocumentExtension4 && ((IDocumentExtension4) fDocumentUndoManager.fDocument).getActiveRewriteSession() == null) { DocumentRewriteSessionType sessionType= size > 1000 ? DocumentRewriteSessionType.UNRESTRICTED : DocumentRewriteSessionType.UNRESTRICTED_SMALL; rewriteSession= ((IDocumentExtension4) fDocumentUndoManager.fDocument).startRewriteSession(sessionType); } for (int i= size - 1; i >= 0; --i) { c= fChanges.get(i); c.undoTextChange(); } if (rewriteSession != null) { ((IDocumentExtension4) fDocumentUndoManager.fDocument).stopRewriteSession(rewriteSession); } fDocumentUndoManager.resetProcessChangeState(); fDocumentUndoManager.fireDocumentUndo(c.fStart, c.fPreservedText, c.fText, uiInfo, DocumentUndoEvent.UNDONE, size > 1); } return Status.OK_STATUS; } @Override public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) { int size= fChanges.size(); if (size > 0) { UndoableTextChange c; c= fChanges.get(size - 1); fDocumentUndoManager.fireDocumentUndo(c.fStart, c.fText, c.fPreservedText, uiInfo, DocumentUndoEvent.ABOUT_TO_REDO, size > 1); DocumentRewriteSession rewriteSession= null; if (size > 25 && fDocumentUndoManager.fDocument instanceof IDocumentExtension4 && ((IDocumentExtension4) fDocumentUndoManager.fDocument).getActiveRewriteSession() == null) { DocumentRewriteSessionType sessionType= size > 1000 ? DocumentRewriteSessionType.UNRESTRICTED : DocumentRewriteSessionType.UNRESTRICTED_SMALL; rewriteSession= ((IDocumentExtension4) fDocumentUndoManager.fDocument).startRewriteSession(sessionType); } for (int i= 0; i < size; ++i) { c= fChanges.get(i); c.redoTextChange(); } if (rewriteSession != null) { ((IDocumentExtension4) fDocumentUndoManager.fDocument).stopRewriteSession(rewriteSession); } fDocumentUndoManager.resetProcessChangeState(); fDocumentUndoManager.fireDocumentUndo(c.fStart, c.fText, c.fPreservedText, uiInfo, DocumentUndoEvent.REDONE, size > 1); } return Status.OK_STATUS; } @Override protected void updateTextChange() { // first gather the data from the buffers super.updateTextChange(); // the result of the update is stored as a child change UndoableTextChange c= new UndoableTextChange(fDocumentUndoManager); c.fStart= fStart; c.fEnd= fEnd; c.fText= fText; c.fPreservedText= fPreservedText; c.fUndoModificationStamp= fUndoModificationStamp; c.fRedoModificationStamp= fRedoModificationStamp; add(c); // clear out all indexes now that the child is added reinitialize(); } @Override protected UndoableTextChange createCurrent() { if (!fDocumentUndoManager.fFoldingIntoCompoundChange) { return new UndoableTextChange(fDocumentUndoManager); } reinitialize(); return this; } @Override protected void commit() { // if there is pending data, update the text change if (fStart > -1) { updateTextChange(); } fDocumentUndoManager.fCurrent= createCurrent(); fDocumentUndoManager.resetProcessChangeState(); } @Override protected boolean isValid() { return fStart > -1 || !fChanges.isEmpty(); } @Override protected long getUndoModificationStamp() { if (fStart > -1) { return super.getUndoModificationStamp(); } else if (!fChanges.isEmpty()) { return fChanges.get(0) .getUndoModificationStamp(); } return fUndoModificationStamp; } @Override protected long getRedoModificationStamp() { if (fStart > -1) { return super.getRedoModificationStamp(); } else if (!fChanges.isEmpty()) { return fChanges.get(fChanges.size() - 1) .getRedoModificationStamp(); } return fRedoModificationStamp; } }
Internal listener to document changes.
/** * Internal listener to document changes. */
private class DocumentListener implements IDocumentListener { private String fReplacedText; @Override public void documentAboutToBeChanged(DocumentEvent event) { try { fReplacedText= event.getDocument().get(event.getOffset(), event.getLength()); fPreservedUndoModificationStamp= event.getModificationStamp(); } catch (BadLocationException x) { fReplacedText= null; } } @Override public void documentChanged(DocumentEvent event) { fPreservedRedoModificationStamp= event.getModificationStamp(); // record the current valid state for the top operation in case it // remains the // top operation but changes state. IUndoableOperation op= fHistory.getUndoOperation(fUndoContext); boolean wasValid= false; if (op != null) { wasValid= op.canUndo(); } // Process the change, providing the before and after timestamps processChange(event.getOffset(), event.getOffset() + event.getLength(), event.getText(), fReplacedText, fPreservedUndoModificationStamp, fPreservedRedoModificationStamp); // now update fCurrent with the latest buffers from the document // change. fCurrent.pretendCommit(); if (op == fCurrent) { // if the document change did not cause a new fCurrent to be // created, then we should // notify the history that the current operation changed if its // validity has changed. if (wasValid != fCurrent.isValid()) { fHistory.operationChanged(op); } } else { // if the change created a new fCurrent that we did not yet add // to the // stack, do so if it's valid and we are not in the middle of a // compound change. if (fCurrent != fLastAddedTextEdit && fCurrent.isValid()) { addToOperationHistory(fCurrent); } } } } /* * @see IOperationHistoryListener */ private class HistoryListener implements IOperationHistoryListener { private IUndoableOperation fOperation; @Override public void historyNotification(final OperationHistoryEvent event) { final int type= event.getEventType(); switch (type) { case OperationHistoryEvent.ABOUT_TO_UNDO: case OperationHistoryEvent.ABOUT_TO_REDO: // if this is one of our operations if (event.getOperation().hasContext(fUndoContext)) { // if we are undoing/redoing an operation we generated, then // ignore // the document changes associated with this undo or redo. if (event.getOperation() instanceof UndoableTextChange) { listenToTextChanges(false); // in the undo case only, make sure compounds are closed if (type == OperationHistoryEvent.ABOUT_TO_UNDO) { if (fFoldingIntoCompoundChange) { endCompoundChange(); } } } else { // the undo or redo has our context, but it is not one // of our edits. We will listen to the changes, but will // reset the state that tracks the undo/redo history. commit(); fLastAddedTextEdit= null; } fOperation= event.getOperation(); } break; case OperationHistoryEvent.UNDONE: case OperationHistoryEvent.REDONE: case OperationHistoryEvent.OPERATION_NOT_OK: if (event.getOperation() == fOperation) { listenToTextChanges(true); fOperation= null; } break; } } }
The undo context for this document undo manager.
/** * The undo context for this document undo manager. */
private ObjectUndoContext fUndoContext;
The document whose changes are being tracked.
/** * The document whose changes are being tracked. */
private IDocument fDocument;
The currently constructed edit.
/** * The currently constructed edit. */
private UndoableTextChange fCurrent;
The internal document listener.
/** * The internal document listener. */
private DocumentListener fDocumentListener;
Indicates whether the current change belongs to a compound change.
/** * Indicates whether the current change belongs to a compound change. */
private boolean fFoldingIntoCompoundChange= false;
The operation history being used to store the undo history.
/** * The operation history being used to store the undo history. */
private IOperationHistory fHistory;
The operation history listener used for managing undo and redo before and after the individual edits are performed.
/** * The operation history listener used for managing undo and redo before and * after the individual edits are performed. */
private IOperationHistoryListener fHistoryListener;
The text edit last added to the operation history. This must be tracked internally instead of asking the history, since outside parties may be placing items on our undo/redo history.
/** * The text edit last added to the operation history. This must be tracked * internally instead of asking the history, since outside parties may be * placing items on our undo/redo history. */
private UndoableTextChange fLastAddedTextEdit= null;
The document modification stamp for redo.
/** * The document modification stamp for redo. */
private long fPreservedRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
Text buffer to collect viewer content which has been replaced
/** * Text buffer to collect viewer content which has been replaced */
private StringBuilder fPreservedTextBuffer;
The document modification stamp for undo.
/** * The document modification stamp for undo. */
private long fPreservedUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
The last delete text edit.
/** * The last delete text edit. */
private UndoableTextChange fPreviousDelete;
Text buffer to collect text which is inserted into the viewer
/** * Text buffer to collect text which is inserted into the viewer */
private StringBuilder fTextBuffer;
Indicates inserting state.
/** Indicates inserting state. */
private boolean fInserting= false;
Indicates overwriting state.
/** Indicates overwriting state. */
private boolean fOverwriting= false;
The registered document listeners.
/** The registered document listeners. */
private ListenerList<IDocumentUndoListener> fDocumentUndoListeners;
The list of clients connected.
/** The list of clients connected. */
private List<Object> fConnected;
Create a DocumentUndoManager for the given document.
Params:
  • document – the document whose undo history is being managed.
/** * * Create a DocumentUndoManager for the given document. * * @param document the document whose undo history is being managed. */
public DocumentUndoManager(IDocument document) { super(); Assert.isNotNull(document); fDocument= document; fHistory= OperationHistoryFactory.getOperationHistory(); fUndoContext= new ObjectUndoContext(fDocument); fConnected= new ArrayList<>(); fDocumentUndoListeners= new ListenerList<>(ListenerList.IDENTITY); } @Override public void addDocumentUndoListener(IDocumentUndoListener listener) { fDocumentUndoListeners.add(listener); } @Override public void removeDocumentUndoListener(IDocumentUndoListener listener) { fDocumentUndoListeners.remove(listener); } @Override public IUndoContext getUndoContext() { return fUndoContext; } @Override public void commit() { // if fCurrent has never been placed on the history, do so now. // this can happen when there are multiple programmatically commits in a // single document change. if (fLastAddedTextEdit != fCurrent) { fCurrent.pretendCommit(); if (fCurrent.isValid()) { addToOperationHistory(fCurrent); } } fCurrent.commit(); } @Override public void reset() { if (isConnected()) { shutdown(); initialize(); } } @Override public boolean redoable() { return OperationHistoryFactory.getOperationHistory().canRedo(fUndoContext); } @Override public boolean undoable() { return OperationHistoryFactory.getOperationHistory().canUndo(fUndoContext); } /* * @see org.eclipse.text.undo.IDocumentUndoManager#undo() */ @Override public void redo() throws ExecutionException { if (isConnected() && redoable()) { OperationHistoryFactory.getOperationHistory().redo(getUndoContext(), null, null); } } @Override public void undo() throws ExecutionException { if (undoable()) { OperationHistoryFactory.getOperationHistory().undo(fUndoContext, null, null); } } @Override public void connect(Object client) { if (!isConnected()) { initialize(); } if (!fConnected.contains(client)) { fConnected.add(client); } } @Override public void disconnect(Object client) { fConnected.remove(client); if (!isConnected()) { shutdown(); } } @Override public void beginCompoundChange() { if (isConnected()) { fFoldingIntoCompoundChange= true; commit(); } } @Override public void endCompoundChange() { if (isConnected()) { fFoldingIntoCompoundChange= false; commit(); } } /* * @see org.eclipse.jface.text.IDocumentUndoManager#setUndoLimit(int) */ @Override public void setMaximalUndoLevel(int undoLimit) { fHistory.setLimit(fUndoContext, undoLimit); }
Fires a document undo event to all registered document undo listeners. Uses a robust iterator.
Params:
  • offset – the document offset
  • text – the text that was inserted
  • preservedText – the text being replaced
  • source – the source which triggered the event
  • eventType – the type of event causing the change
  • isCompound – a flag indicating whether the change is a compound change
See Also:
/** * Fires a document undo event to all registered document undo listeners. * Uses a robust iterator. * * @param offset the document offset * @param text the text that was inserted * @param preservedText the text being replaced * @param source the source which triggered the event * @param eventType the type of event causing the change * @param isCompound a flag indicating whether the change is a compound change * @see IDocumentUndoListener */
void fireDocumentUndo(int offset, String text, String preservedText, Object source, int eventType, boolean isCompound) { eventType= isCompound ? eventType | DocumentUndoEvent.COMPOUND : eventType; DocumentUndoEvent event= new DocumentUndoEvent(fDocument, offset, text, preservedText, eventType, source); for (IDocumentUndoListener listener : fDocumentUndoListeners) { listener.documentUndoNotification(event); } }
Adds any listeners needed to track the document and the operations history.
/** * Adds any listeners needed to track the document and the operations * history. */
private void addListeners() { fHistoryListener= new HistoryListener(); fHistory.addOperationHistoryListener(fHistoryListener); listenToTextChanges(true); }
Removes any listeners that were installed by the document.
/** * Removes any listeners that were installed by the document. */
private void removeListeners() { listenToTextChanges(false); fHistory.removeOperationHistoryListener(fHistoryListener); fHistoryListener= null; }
Adds the given text edit to the operation history if it is not part of a compound change.
Params:
  • edit – the edit to be added
/** * Adds the given text edit to the operation history if it is not part of a compound change. * * @param edit the edit to be added */
private void addToOperationHistory(UndoableTextChange edit) { if (!fFoldingIntoCompoundChange || edit instanceof UndoableCompoundTextChange) { fHistory.add(edit); fLastAddedTextEdit= edit; } }
Disposes the undo history.
/** * Disposes the undo history. */
private void disposeUndoHistory() { fHistory.dispose(fUndoContext, true, true, true); }
Initializes the undo history.
/** * Initializes the undo history. */
private void initializeUndoHistory() { if (fHistory != null && fUndoContext != null) { fHistory.dispose(fUndoContext, true, true, false); } }
Checks whether the given text starts with a line delimiter and subsequently contains a white space only.
Params:
  • text – the text to check
Returns:true if the text is a line delimiter followed by whitespace, false otherwise
/** * Checks whether the given text starts with a line delimiter and * subsequently contains a white space only. * * @param text the text to check * @return <code>true</code> if the text is a line delimiter followed by * whitespace, <code>false</code> otherwise */
private boolean isWhitespaceText(String text) { if (text == null || text.isEmpty()) { return false; } String[] delimiters= fDocument.getLegalLineDelimiters(); int index= TextUtilities.startsWith(delimiters, text); if (index > -1) { char c; int length= text.length(); for (int i= delimiters[index].length(); i < length; i++) { c= text.charAt(i); if (c != ' ' && c != '\t') { return false; } } return true; } return false; }
Switches the state of whether there is a text listener or not.
Params:
  • listen – the state which should be established
/** * Switches the state of whether there is a text listener or not. * * @param listen the state which should be established */
private void listenToTextChanges(boolean listen) { if (listen) { if (fDocumentListener == null && fDocument != null) { fDocumentListener= new DocumentListener(); fDocument.addDocumentListener(fDocumentListener); } } else if (!listen) { if (fDocumentListener != null && fDocument != null) { fDocument.removeDocumentListener(fDocumentListener); fDocumentListener= null; } } } private void processChange(int modelStart, int modelEnd, String insertedText, String replacedText, final long beforeChangeModificationStamp, final long afterChangeModificationStamp) { if (insertedText == null) { insertedText= ""; //$NON-NLS-1$ } if (replacedText == null) { replacedText= ""; //$NON-NLS-1$ } int length= insertedText.length(); int diff= modelEnd - modelStart; if (fCurrent.fUndoModificationStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) { fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; } // normalize if (diff < 0) { int tmp= modelEnd; modelEnd= modelStart; modelStart= tmp; } if (modelStart == modelEnd) { // text will be inserted if ((length == 1) || isWhitespaceText(insertedText)) { // by typing or whitespace if (!fInserting || (modelStart != fCurrent.fStart + fTextBuffer.length())) { fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; if (fCurrent.attemptCommit()) { fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; } fInserting= true; } if (fCurrent.fStart < 0) { fCurrent.fStart= fCurrent.fEnd= modelStart; } if (length > 0) { fTextBuffer.append(insertedText); } } else if (length > 0) { // by pasting or model manipulation fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; if (fCurrent.attemptCommit()) { fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; } fCurrent.fStart= fCurrent.fEnd= modelStart; fTextBuffer.append(insertedText); fCurrent.fRedoModificationStamp= afterChangeModificationStamp; if (fCurrent.attemptCommit()) { fCurrent.fUndoModificationStamp= afterChangeModificationStamp; } } } else { if (length == 0) { // text will be deleted by backspace or DEL key or empty // clipboard length= replacedText.length(); String[] delimiters= fDocument.getLegalLineDelimiters(); if ((length == 1) || TextUtilities.equals(delimiters, replacedText) > -1) { // whereby selection is empty if (fPreviousDelete.fStart == modelStart && fPreviousDelete.fEnd == modelEnd) { // repeated DEL // correct wrong settings of fCurrent if (fCurrent.fStart == modelEnd && fCurrent.fEnd == modelStart) { fCurrent.fStart= modelStart; fCurrent.fEnd= modelEnd; } // append to buffer && extend edit range fPreservedTextBuffer.append(replacedText); ++fCurrent.fEnd; } else if (fPreviousDelete.fStart == modelEnd) { // repeated backspace // insert in buffer and extend edit range fPreservedTextBuffer.insert(0, replacedText); fCurrent.fStart= modelStart; } else { // either DEL or backspace for the first time fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; if (fCurrent.attemptCommit()) { fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; } // as we can not decide whether it was DEL or backspace // we initialize for backspace fPreservedTextBuffer.append(replacedText); fCurrent.fStart= modelStart; fCurrent.fEnd= modelEnd; } fPreviousDelete.set(modelStart, modelEnd); } else if (length > 0) { // whereby selection is not empty fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; if (fCurrent.attemptCommit()) { fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; } fCurrent.fStart= modelStart; fCurrent.fEnd= modelEnd; fPreservedTextBuffer.append(replacedText); } } else { // text will be replaced if (length == 1) { length= replacedText.length(); String[] delimiters= fDocument.getLegalLineDelimiters(); if ((length == 1) || TextUtilities.equals(delimiters, replacedText) > -1) { // because of overwrite mode or model manipulation if (!fOverwriting || (modelStart != fCurrent.fStart + fTextBuffer.length())) { fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; if (fCurrent.attemptCommit()) { fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; } fOverwriting= true; } if (fCurrent.fStart < 0) { fCurrent.fStart= modelStart; } fCurrent.fEnd= modelEnd; fTextBuffer.append(insertedText); fPreservedTextBuffer.append(replacedText); fCurrent.fRedoModificationStamp= afterChangeModificationStamp; return; } } // because of typing or pasting whereby selection is not empty fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; if (fCurrent.attemptCommit()) { fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; } fCurrent.fStart= modelStart; fCurrent.fEnd= modelEnd; fTextBuffer.append(insertedText); fPreservedTextBuffer.append(replacedText); } } // in all cases, the redo modification stamp is updated on the open // text edit fCurrent.fRedoModificationStamp= afterChangeModificationStamp; }
Initialize the receiver.
/** * Initialize the receiver. */
private void initialize() { initializeUndoHistory(); // open up the current text edit fCurrent= new UndoableTextChange(this); fPreviousDelete= new UndoableTextChange(this); fTextBuffer= new StringBuilder(); fPreservedTextBuffer= new StringBuilder(); addListeners(); }
Reset processChange state.
Since:3.2
/** * Reset processChange state. * * @since 3.2 */
private void resetProcessChangeState() { fInserting= false; fOverwriting= false; fPreviousDelete.reinitialize(); }
Shutdown the receiver.
/** * Shutdown the receiver. */
private void shutdown() { removeListeners(); fCurrent= null; fPreviousDelete= null; fTextBuffer= null; fPreservedTextBuffer= null; disposeUndoHistory(); }
Return whether or not any clients are connected to the receiver.
Returns:true if the receiver is connected to clients, false if it is not
/** * Return whether or not any clients are connected to the receiver. * * @return <code>true</code> if the receiver is connected to * clients, <code>false</code> if it is not */
boolean isConnected() { if (fConnected == null) { return false; } return !fConnected.isEmpty(); } @Override public void transferUndoHistory(IDocumentUndoManager manager) { IUndoContext oldUndoContext= manager.getUndoContext(); // Get the history for the old undo context. IUndoableOperation [] operations= OperationHistoryFactory.getOperationHistory().getUndoHistory(oldUndoContext); for (IUndoableOperation operation : operations) { // First replace the undo context IUndoableOperation op= operation; if (op instanceof IContextReplacingOperation) { ((IContextReplacingOperation)op).replaceContext(oldUndoContext, getUndoContext()); } else { op.addContext(getUndoContext()); op.removeContext(oldUndoContext); } // Now update the manager that owns the text edit. if (op instanceof UndoableTextChange) { ((UndoableTextChange)op).fDocumentUndoManager= this; } } IUndoableOperation op= OperationHistoryFactory.getOperationHistory().getUndoOperation(getUndoContext()); if (op != null && !(op instanceof UndoableTextChange)) { return; } // Record the transfer itself as an undoable change. // If the transfer results from some open operation, recording this change will // cause our undo context to be added to the outer operation. If there is no // outer operation, there will be a local change to signify the transfer. // This also serves to synchronize the modification stamps with the documents. UndoableTextChange cmd= new UndoableTextChange(this); cmd.fStart= cmd.fEnd= 0; cmd.fText= cmd.fPreservedText= ""; //$NON-NLS-1$ if (fDocument instanceof IDocumentExtension4) { cmd.fRedoModificationStamp= ((IDocumentExtension4)fDocument).getModificationStamp(); if (op != null) { cmd.fUndoModificationStamp= ((UndoableTextChange)op).fRedoModificationStamp; } } addToOperationHistory(cmd); } }