org.eclipse.text.undo.DocumentUndoManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aspectjtools Show documentation
Show all versions of aspectjtools Show documentation
Tools from the AspectJ project
/*******************************************************************************
* 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 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.
*
*/
private static class UndoableTextChange extends AbstractOperation {
/** The start index of the replaced text. */
protected int fStart= -1;
/** The end index of the replaced text. */
protected int fEnd= -1;
/** The newly inserted text. */
protected String fText;
/** The replaced text. */
protected String fPreservedText;
/** The undo modification stamp. */
protected long fUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
/** The redo modification stamp. */
protected long fRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
/** The undo manager that generated the change. */
protected DocumentUndoManager fDocumentUndoManager;
/**
* 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.
*/
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.
*
* @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.
*/
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.
*/
@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.
*/
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.
*
* @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.
*/
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.
*
* @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.
*/
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.
*/
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.
*
* @return true
if the change was committed and created
* a new fCurrent
, false
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.
*
* @return true
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
*
* @return the undo modification stamp for this change
*/
protected long getUndoModificationStamp() {
return fUndoModificationStamp;
}
/**
* 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.
*/
private static class UndoableCompoundTextChange extends UndoableTextChange {
/** The list of individual changes */
private List fChanges= new ArrayList<>();
/**
* 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.
*
* @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.
*/
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) {
long 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.
*/
private ObjectUndoContext fUndoContext;
/**
* The document whose changes are being tracked.
*/
private IDocument fDocument;
/**
* The currently constructed edit.
*/
private UndoableTextChange fCurrent;
/**
* The internal document listener.
*/
private DocumentListener fDocumentListener;
/**
* Indicates whether the current change belongs to a compound change.
*/
private boolean fFoldingIntoCompoundChange= false;
/**
* 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.
*/
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.
*/
private UndoableTextChange fLastAddedTextEdit= null;
/**
* Text buffer to collect viewer content which has been replaced
*/
private StringBuilder fPreservedTextBuffer;
/**
* The document modification stamp for undo.
*/
private long fPreservedUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
/**
* The last delete text edit.
*/
private UndoableTextChange fPreviousDelete;
/**
* Text buffer to collect text which is inserted into the viewer
*/
private StringBuilder fTextBuffer;
/** Indicates inserting state. */
private boolean fInserting= false;
/** Indicates overwriting state. */
private boolean fOverwriting= false;
/** The registered document listeners. */
private ListenerList fDocumentUndoListeners;
/** The list of clients connected. */
private List
© 2015 - 2024 Weber Informatics LLC | Privacy Policy