blob: 4ead31215b14318c46cf15583677879c3dd0435f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2015 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
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.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.
* <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.
* <p>
* Based on the DefaultUndoManager.TextCommand from R3.1.
* </p>
*/
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 <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.
*
* @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$
StringBuffer text= new StringBuffer(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<UndoableTextChange> 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, true);
for (int i= size - 1; i >= 0; --i) {
c= fChanges.get(i);
c.undoTextChange();
}
fDocumentUndoManager.resetProcessChangeState();
fDocumentUndoManager.fireDocumentUndo(c.fStart, c.fPreservedText, c.fText, uiInfo,
DocumentUndoEvent.UNDONE, true);
}
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, true);
for (int i= 0; i <= size - 1; ++i) {
c= fChanges.get(i);
c.redoTextChange();
}
fDocumentUndoManager.resetProcessChangeState();
fDocumentUndoManager.fireDocumentUndo(c.fStart, c.fText, c.fPreservedText, uiInfo, DocumentUndoEvent.REDONE, true);
}
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.size() > 0;
}
@Override
protected long getUndoModificationStamp() {
if (fStart > -1)
return super.getUndoModificationStamp();
else if (fChanges.size() > 0)
return fChanges.get(0)
.getUndoModificationStamp();
return fUndoModificationStamp;
}
@Override
protected long getRedoModificationStamp() {
if (fStart > -1)
return super.getRedoModificationStamp();
else if (fChanges.size() > 0)
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) {
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;
/**
* The document modification stamp for redo.
*/
private long fPreservedRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
/**
* Text buffer to collect viewer content which has been replaced
*/
private StringBuffer 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 StringBuffer 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<Object> fConnected;
/**
*
* 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.
*
* @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);
Object[] listeners= fDocumentUndoListeners.getListeners();
for (int i= 0; i < listeners.length; i++) {
((IDocumentUndoListener)listeners[i]).documentUndoNotification(event);
}
}
/**
* 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.
*/
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.
*
* @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.
*/
private void disposeUndoHistory() {
fHistory.dispose(fUndoContext, true, true, true);
}
/**
* 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.
*
* @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.length() == 0)
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.
*
* @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.
*/
private void initialize() {
initializeUndoHistory();
// open up the current text edit
fCurrent= new UndoableTextChange(this);
fPreviousDelete= new UndoableTextChange(this);
fTextBuffer= new StringBuffer();
fPreservedTextBuffer= new StringBuffer();
addListeners();
}
/**
* Reset processChange state.
*
* @since 3.2
*/
private void resetProcessChangeState() {
fInserting= false;
fOverwriting= false;
fPreviousDelete.reinitialize();
}
/**
* 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.
*
* @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 (int i= 0; i < operations.length; i++) {
// First replace the undo context
IUndoableOperation op= operations[i];
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);
}
}