org.eclipse.text.undo.DocumentUndoManager Maven / Gradle / Ivy
/*******************************************************************************
* 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;
@SuppressWarnings("incomplete-switch")
@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