blob: fb9b366a6f8226cb18ad1c3aa1896caeb70b3687 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 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.jface.text;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.AbstractOperation;
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.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.MessageDialog;
/**
* Standard implementation of {@link org.eclipse.jface.text.IUndoManager}.
* <p>
* It registers with the connected text viewer as text input listener and
* document listener and logs all changes. It also monitors mouse and keyboard
* activities in order to partition the stream of text changes into undo-able
* edit commands.
* </p>
* <p>
* Since 3.1 this undo manager is a facade to the global operation history.
* </p>
* <p>
* The usage of {@link org.eclipse.core.runtime.IAdaptable} in the JFace
* layer has been approved by Platform UI, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=87669#c9
* </p>
* <p>
* This class is not intended to be subclassed.
* </p>
*
* @see org.eclipse.jface.text.ITextViewer
* @see org.eclipse.jface.text.ITextInputListener
* @see org.eclipse.jface.text.IDocumentListener
* @see org.eclipse.core.commands.operations.IUndoableOperation
* @see org.eclipse.core.commands.operations.IOperationHistory
* @see MouseListener
* @see KeyListener
* @deprecated As of 3.2, replaced by {@link TextViewerUndoManager}
* @noextend This class is not intended to be subclassed by clients.
*/
@Deprecated
public class DefaultUndoManager implements IUndoManager, IUndoManagerExtension {
/**
* Represents an undo-able edit command.
* <p>
* Since 3.1 this implements the interface for IUndoableOperation.
* </p>
*/
class TextCommand 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;
/**
* Creates a new text command.
*
* @param context the undo context for this command
* @since 3.1
*/
TextCommand(IUndoContext context) {
super(JFaceTextMessages.getString("DefaultUndoManager.operationLabel")); //$NON-NLS-1$
addContext(context);
}
/**
* Re-initializes this text command.
*/
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 command.
*
* @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 command.
*
* @since 2.0
*/
protected void undoTextChange() {
try {
IDocument document= fTextViewer.getDocument();
if (document instanceof IDocumentExtension4)
((IDocumentExtension4)document).replace(fStart, fText.length(), fPreservedText, fUndoModificationStamp);
else
document.replace(fStart, fText.length(), fPreservedText);
} catch (BadLocationException x) {
}
}
@Override
public boolean canUndo() {
if (isConnected() && isValid()) {
IDocument doc= fTextViewer.getDocument();
if (doc instanceof IDocumentExtension4) {
long docStamp= ((IDocumentExtension4)doc).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 == fHistory.getUndoOperation(fUndoContext) && // this is the latest operation
this != fCurrent && // there is a more current operation not on the stack
!fCurrent.isValid() && // the current operation is not a valid document modification
fCurrent.fUndoModificationStamp != // the invalid current operation has a document stamp
IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) {
canUndo= fCurrent.fRedoModificationStamp == docStamp;
}
/*
* When the composite is the current command, it may hold the timestamp
* of a no-op change. We check this here rather than in an override of
* canUndo() in CompoundTextCommand simply to keep all the special case checks
* in one place.
*/
if (!canUndo &&
this == fHistory.getUndoOperation(fUndoContext) && // this is the latest operation
this instanceof CompoundTextCommand &&
this == fCurrent && // this is the current operation
this.fStart == -1 && // the current operation text is not valid
fCurrent.fRedoModificationStamp != IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) { // but it has a redo stamp
canUndo= fCurrent.fRedoModificationStamp == docStamp;
}
}
// 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 (isConnected() && isValid()) {
IDocument doc= fTextViewer.getDocument();
if (doc instanceof IDocumentExtension4) {
long docStamp= ((IDocumentExtension4)doc).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 isConnected();
}
@Override
public IStatus execute(IProgressMonitor monitor, IAdaptable uiInfo) {
// Text commands execute as they are typed, so executing one has no effect.
return Status.OK_STATUS;
}
/*
* Undo the change described by this command. Also selects and
* reveals the change.
*/
/**
* Undo the change described by this command. Also selects and
* reveals the change.
*
* @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 undo(IProgressMonitor monitor, IAdaptable uiInfo) {
if (isValid()) {
undoTextChange();
selectAndReveal(fStart, fPreservedText == null ? 0 : fPreservedText.length());
resetProcessChangeSate();
return Status.OK_STATUS;
}
return IOperationHistory.OPERATION_INVALID_STATUS;
}
/**
* Re-applies the change described by this command.
*
* @since 2.0
*/
protected void redoTextChange() {
try {
IDocument document= fTextViewer.getDocument();
if (document instanceof IDocumentExtension4)
((IDocumentExtension4)document).replace(fStart, fEnd - fStart, fText, fRedoModificationStamp);
else
fTextViewer.getDocument().replace(fStart, fEnd - fStart, fText);
} catch (BadLocationException x) {
}
}
/**
* Re-applies the change described by this command that previously been
* rolled back. Also selects and reveals the change.
*
* @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()) {
redoTextChange();
resetProcessChangeSate();
selectAndReveal(fStart, fText == null ? 0 : fText.length());
return Status.OK_STATUS;
}
return IOperationHistory.OPERATION_INVALID_STATUS;
}
/**
* Update the command in response to a commit.
*
* @since 3.1
*/
protected void updateCommand() {
fText= fTextBuffer.toString();
fTextBuffer.setLength(0);
fPreservedText= fPreservedTextBuffer.toString();
fPreservedTextBuffer.setLength(0);
}
/**
* Creates a new uncommitted text command depending on whether
* a compound change is currently being executed.
*
* @return a new, uncommitted text command or a compound text command
*/
protected TextCommand createCurrent() {
return fFoldingIntoCompoundChange ? new CompoundTextCommand(fUndoContext) : new TextCommand(fUndoContext);
}
/**
* Commits the current change into this command.
*/
protected void commit() {
if (fStart < 0) {
if (fFoldingIntoCompoundChange) {
fCurrent= createCurrent();
} else {
reinitialize();
}
} else {
updateCommand();
fCurrent= createCurrent();
}
resetProcessChangeSate();
}
/**
* Updates the text from the buffers without resetting
* the buffers or adding anything to the stack.
*
* @since 3.1
*/
protected void pretendCommit() {
if (fStart > -1) {
fText= fTextBuffer.toString();
fPreservedText= fPreservedTextBuffer.toString();
}
}
/**
* Attempt a commit of this command and answer true if a new
* fCurrent was created as a result of the commit.
*
* @return true if the command was committed and created a
* new fCurrent, false if not.
* @since 3.1
*/
protected boolean attemptCommit() {
pretendCommit();
if (isValid()) {
DefaultUndoManager.this.commit();
return true;
}
return false;
}
/**
* Checks whether this text command is valid for undo or redo.
*
* @return <code>true</code> if the command is valid for undo or redo
* @since 3.1
*/
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 command
* @since 3.1
*/
protected long getUndoModificationStamp() {
return fUndoModificationStamp;
}
/**
* Return the redo modification stamp
*
* @return the redo modification stamp for this command
* @since 3.1
*/
protected long getRedoModificationStamp() {
return fRedoModificationStamp;
}
}
/**
* Represents an undo-able edit command consisting of several
* individual edit commands.
*/
class CompoundTextCommand extends TextCommand {
/** The list of individual commands */
private List<TextCommand> fCommands= new ArrayList<>();
/**
* Creates a new compound text command.
*
* @param context the undo context for this command
* @since 3.1
*/
CompoundTextCommand(IUndoContext context) {
super(context);
}
/**
* Adds a new individual command to this compound command.
*
* @param command the command to be added
*/
protected void add(TextCommand command) {
fCommands.add(command);
}
@Override
public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) {
resetProcessChangeSate();
int size= fCommands.size();
if (size > 0) {
TextCommand c;
for (int i= size -1; i > 0; --i) {
c= fCommands.get(i);
c.undoTextChange();
}
c= fCommands.get(0);
c.undo(monitor, uiInfo);
}
return Status.OK_STATUS;
}
@Override
public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) {
resetProcessChangeSate();
int size= fCommands.size();
if (size > 0) {
TextCommand c;
for (int i= 0; i < size -1; ++i) {
c= fCommands.get(i);
c.redoTextChange();
}
c= fCommands.get(size -1);
c.redo(monitor, uiInfo);
}
return Status.OK_STATUS;
}
/*
* @see TextCommand#updateCommand
*/
@Override
protected void updateCommand() {
// first gather the data from the buffers
super.updateCommand();
// the result of the command update is stored as a child command
TextCommand c= new TextCommand(fUndoContext);
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();
}
/*
* @see TextCommand#createCurrent
*/
@Override
protected TextCommand createCurrent() {
if (!fFoldingIntoCompoundChange)
return new TextCommand(fUndoContext);
reinitialize();
return this;
}
@Override
protected void commit() {
// if there is pending data, update the command
if (fStart > -1)
updateCommand();
fCurrent= createCurrent();
resetProcessChangeSate();
}
/**
* Checks whether the command is valid for undo or redo.
*
* @return true if the command is valid.
* @since 3.1
*/
@Override
protected boolean isValid() {
if (isConnected())
return (fStart > -1 || fCommands.size() > 0);
return false;
}
/**
* Returns the undo modification stamp.
*
* @return the undo modification stamp
* @since 3.1
*/
@Override
protected long getUndoModificationStamp() {
if (fStart > -1)
return super.getUndoModificationStamp();
else if (fCommands.size() > 0)
return fCommands.get(0).getUndoModificationStamp();
return fUndoModificationStamp;
}
/**
* Returns the redo modification stamp.
*
* @return the redo modification stamp
* @since 3.1
*/
@Override
protected long getRedoModificationStamp() {
if (fStart > -1)
return super.getRedoModificationStamp();
else if (fCommands.size() > 0)
return fCommands.get(fCommands.size()-1).getRedoModificationStamp();
return fRedoModificationStamp;
}
}
/**
* Internal listener to mouse and key events.
*/
class KeyAndMouseListener implements MouseListener, KeyListener {
/*
* @see MouseListener#mouseDoubleClick
*/
@Override
public void mouseDoubleClick(MouseEvent e) {
}
/*
* If the right mouse button is pressed, the current editing command is closed
* @see MouseListener#mouseDown
*/
@Override
public void mouseDown(MouseEvent e) {
if (e.button == 1)
commit();
}
/*
* @see MouseListener#mouseUp
*/
@Override
public void mouseUp(MouseEvent e) {
}
/*
* @see KeyListener#keyPressed
*/
@Override
public void keyReleased(KeyEvent e) {
}
/*
* On cursor keys, the current editing command is closed
* @see KeyListener#keyPressed
*/
@Override
public void keyPressed(KeyEvent e) {
switch (e.keyCode) {
case SWT.ARROW_UP:
case SWT.ARROW_DOWN:
case SWT.ARROW_LEFT:
case SWT.ARROW_RIGHT:
commit();
break;
}
}
}
/**
* Internal listener to document changes.
*/
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 != fLastAddedCommand && fCurrent.isValid()) {
addToCommandStack(fCurrent);
}
}
}
}
/**
* Internal text input listener.
*/
class TextInputListener implements ITextInputListener {
@Override
public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
if (oldInput != null && fDocumentListener != null) {
oldInput.removeDocumentListener(fDocumentListener);
commit();
}
}
@Override
public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
if (newInput != null) {
if (fDocumentListener == null)
fDocumentListener= new DocumentListener();
newInput.addDocumentListener(fDocumentListener);
}
}
}
/*
* @see IOperationHistoryListener
* @since 3.1
*/
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)) {
fTextViewer.getTextWidget().getDisplay().syncExec(new Runnable() {
@Override
public void run() {
// if we are undoing/redoing a command we generated, then ignore
// the document changes associated with this undo or redo.
if (event.getOperation() instanceof TextCommand) {
if (fTextViewer instanceof TextViewer)
((TextViewer)fTextViewer).ignoreAutoEditStrategies(true);
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 commands. We will listen to the changes, but will
// reset the state that tracks the undo/redo history.
commit();
fLastAddedCommand= null;
}
}
});
fOperation= event.getOperation();
}
break;
case OperationHistoryEvent.UNDONE:
case OperationHistoryEvent.REDONE:
case OperationHistoryEvent.OPERATION_NOT_OK:
if (event.getOperation() == fOperation) {
fTextViewer.getTextWidget().getDisplay().syncExec(new Runnable() {
@Override
public void run() {
listenToTextChanges(true);
fOperation= null;
if (fTextViewer instanceof TextViewer)
((TextViewer)fTextViewer).ignoreAutoEditStrategies(false);
}
});
}
break;
}
}
}
/** Text buffer to collect text which is inserted into the viewer */
private StringBuffer fTextBuffer;
/** Text buffer to collect viewer content which has been replaced */
private StringBuffer fPreservedTextBuffer;
/** The document modification stamp for undo. */
protected long fPreservedUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
/** The document modification stamp for redo. */
protected long fPreservedRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
/** The internal key and mouse event listener */
private KeyAndMouseListener fKeyAndMouseListener;
/** The internal document listener */
private DocumentListener fDocumentListener;
/** The internal text input listener */
private TextInputListener fTextInputListener;
/** Indicates inserting state */
private boolean fInserting= false;
/** Indicates overwriting state */
private boolean fOverwriting= false;
/** Indicates whether the current change belongs to a compound change */
private boolean fFoldingIntoCompoundChange= false;
/** The text viewer the undo manager is connected to */
private ITextViewer fTextViewer;
/** Supported undo level */
private int fUndoLevel;
/** The currently constructed edit command */
private TextCommand fCurrent;
/** The last delete edit command */
private TextCommand fPreviousDelete;
/**
* The undo context.
* @since 3.1
*/
private IOperationHistory fHistory;
/**
* The operation history.
* @since 3.1
*/
private IUndoContext fUndoContext;
/**
* The operation history listener used for managing undo and redo before
* and after the individual commands are performed.
* @since 3.1
*/
private IOperationHistoryListener fHistoryListener= new HistoryListener();
/**
* The command 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 TextCommand fLastAddedCommand= null;
/**
* Creates a new undo manager who remembers the specified number of edit commands.
*
* @param undoLevel the length of this manager's history
*/
public DefaultUndoManager(int undoLevel) {
fHistory= OperationHistoryFactory.getOperationHistory();
setMaximalUndoLevel(undoLevel);
}
/**
* Returns whether this undo manager is connected to a text viewer.
*
* @return <code>true</code> if connected, <code>false</code> otherwise
* @since 3.1
*/
private boolean isConnected() {
return fTextViewer != null;
}
/*
* @see IUndoManager#beginCompoundChange
*/
@Override
public void beginCompoundChange() {
if (isConnected()) {
fFoldingIntoCompoundChange= true;
commit();
}
}
/*
* @see IUndoManager#endCompoundChange
*/
@Override
public void endCompoundChange() {
if (isConnected()) {
fFoldingIntoCompoundChange= false;
commit();
}
}
/**
* Registers all necessary listeners with the text viewer.
*/
private void addListeners() {
StyledText text= fTextViewer.getTextWidget();
if (text != null) {
fKeyAndMouseListener= new KeyAndMouseListener();
text.addMouseListener(fKeyAndMouseListener);
text.addKeyListener(fKeyAndMouseListener);
fTextInputListener= new TextInputListener();
fTextViewer.addTextInputListener(fTextInputListener);
fHistory.addOperationHistoryListener(fHistoryListener);
listenToTextChanges(true);
}
}
/**
* Unregister all previously installed listeners from the text viewer.
*/
private void removeListeners() {
StyledText text= fTextViewer.getTextWidget();
if (text != null) {
if (fKeyAndMouseListener != null) {
text.removeMouseListener(fKeyAndMouseListener);
text.removeKeyListener(fKeyAndMouseListener);
fKeyAndMouseListener= null;
}
if (fTextInputListener != null) {
fTextViewer.removeTextInputListener(fTextInputListener);
fTextInputListener= null;
}
listenToTextChanges(false);
fHistory.removeOperationHistoryListener(fHistoryListener);
}
}
/**
* Adds the given command to the operation history if it is not part of
* a compound change.
*
* @param command the command to be added
* @since 3.1
*/
private void addToCommandStack(TextCommand command){
if (!fFoldingIntoCompoundChange || command instanceof CompoundTextCommand) {
fHistory.add(command);
fLastAddedCommand= command;
}
}
/**
* Disposes the command stack.
*
* @since 3.1
*/
private void disposeCommandStack() {
fHistory.dispose(fUndoContext, true, true, true);
}
/**
* Initializes the command stack.
*
* @since 3.1
*/
private void initializeCommandStack() {
if (fHistory != null && fUndoContext != null)
fHistory.dispose(fUndoContext, true, true, 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 && fTextViewer.getDocument() != null) {
fDocumentListener= new DocumentListener();
fTextViewer.getDocument().addDocumentListener(fDocumentListener);
}
} else if (!listen) {
if (fDocumentListener != null && fTextViewer.getDocument() != null) {
fTextViewer.getDocument().removeDocumentListener(fDocumentListener);
fDocumentListener= null;
}
}
}
/**
* Closes the current editing command and opens a new one.
*/
private void commit() {
// if fCurrent has never been placed on the command stack, do so now.
// this can happen when there are multiple programmatically commits in a single
// document change.
if (fLastAddedCommand != fCurrent) {
fCurrent.pretendCommit();
if (fCurrent.isValid())
addToCommandStack(fCurrent);
}
fCurrent.commit();
}
/**
* Reset processChange state.
*
* @since 3.2
*/
private void resetProcessChangeSate() {
fInserting= false;
fOverwriting= false;
fPreviousDelete.reinitialize();
}
/**
* 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= fTextViewer.getDocument().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;
}
private void processChange(int modelStart, int modelEnd, String insertedText, String replacedText, long beforeChangeModificationStamp, 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= fTextViewer.getDocument().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 command range
fPreservedTextBuffer.append(replacedText);
++fCurrent.fEnd;
} else if (fPreviousDelete.fStart == modelEnd) {
// repeated backspace
// insert in buffer and extend command 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= fTextViewer.getDocument().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 command
fCurrent.fRedoModificationStamp= afterChangeModificationStamp;
}
/**
* Shows the given exception in an error dialog.
*
* @param title the dialog title
* @param ex the exception
* @since 3.1
*/
private void openErrorDialog(final String title, final Exception ex) {
Shell shell= null;
if (isConnected()) {
StyledText st= fTextViewer.getTextWidget();
if (st != null && !st.isDisposed())
shell= st.getShell();
}
if (Display.getCurrent() != null)
MessageDialog.openError(shell, title, ex.getLocalizedMessage());
else {
Display display;
final Shell finalShell= shell;
if (finalShell != null)
display= finalShell.getDisplay();
else
display= Display.getDefault();
display.syncExec(new Runnable() {
@Override
public void run() {
MessageDialog.openError(finalShell, title, ex.getLocalizedMessage());
}
});
}
}
@Override
public void setMaximalUndoLevel(int undoLevel) {
fUndoLevel= Math.max(0, undoLevel);
if (isConnected()) {
fHistory.setLimit(fUndoContext, fUndoLevel);
}
}
@Override
public void connect(ITextViewer textViewer) {
if (!isConnected() && textViewer != null) {
fTextViewer= textViewer;
fTextBuffer= new StringBuffer();
fPreservedTextBuffer= new StringBuffer();
if (fUndoContext == null)
fUndoContext= new ObjectUndoContext(this);
fHistory.setLimit(fUndoContext, fUndoLevel);
initializeCommandStack();
// open up the current command
fCurrent= new TextCommand(fUndoContext);
fPreviousDelete= new TextCommand(fUndoContext);
addListeners();
}
}
@Override
public void disconnect() {
if (isConnected()) {
removeListeners();
fCurrent= null;
fTextViewer= null;
disposeCommandStack();
fTextBuffer= null;
fPreservedTextBuffer= null;
fUndoContext= null;
}
}
@Override
public void reset() {
if (isConnected()) {
initializeCommandStack();
fCurrent= new TextCommand(fUndoContext);
fFoldingIntoCompoundChange= false;
fInserting= false;
fOverwriting= false;
fTextBuffer.setLength(0);
fPreservedTextBuffer.setLength(0);
fPreservedUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
fPreservedRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
}
}
@Override
public boolean redoable() {
return fHistory.canRedo(fUndoContext);
}
@Override
public boolean undoable() {
return fHistory.canUndo(fUndoContext);
}
@Override
public void redo() {
if (isConnected() && redoable()) {
try {
fHistory.redo(fUndoContext, null, null);
} catch (ExecutionException ex) {
openErrorDialog(JFaceTextMessages.getString("DefaultUndoManager.error.redoFailed.title"), ex); //$NON-NLS-1$
}
}
}
@Override
public void undo() {
if (isConnected() && undoable()) {
try {
fHistory.undo(fUndoContext, null, null);
} catch (ExecutionException ex) {
openErrorDialog(JFaceTextMessages.getString("DefaultUndoManager.error.undoFailed.title"), ex); //$NON-NLS-1$
}
}
}
/**
* Selects and reveals the specified range.
*
* @param offset the offset of the range
* @param length the length of the range
* @since 3.0
*/
protected void selectAndReveal(int offset, int length) {
if (fTextViewer instanceof ITextViewerExtension5) {
ITextViewerExtension5 extension= (ITextViewerExtension5) fTextViewer;
extension.exposeModelRange(new Region(offset, length));
} else if (!fTextViewer.overlapsWithVisibleRegion(offset, length))
fTextViewer.resetVisibleRegion();
fTextViewer.setSelectedRange(offset, length);
fTextViewer.revealRange(offset, length);
}
@Override
public IUndoContext getUndoContext() {
return fUndoContext;
}
}