Mercurial > projects > dwt-addons
diff dwtx/text/undo/DocumentUndoManager.d @ 129:eb30df5ca28b
Added JFace Text sources
author | Frank Benoit <benoit@tionex.de> |
---|---|
date | Sat, 23 Aug 2008 19:10:48 +0200 |
parents | |
children | b56e9be9fe88 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/text/undo/DocumentUndoManager.d Sat Aug 23 19:10:48 2008 +0200 @@ -0,0 +1,1288 @@ +/******************************************************************************* + * Copyright (c) 2006, 2008 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 + * Port to the D programming language: + * Frank Benoit <benoit@tionex.de> + *******************************************************************************/ +module dwtx.text.undo.DocumentUndoManager; + +import dwt.dwthelper.utils; + +import java.util.ArrayList; +import java.util.List; + +import dwtx.core.commands.ExecutionException; +import dwtx.core.commands.operations.AbstractOperation; +import dwtx.core.commands.operations.IContextReplacingOperation; +import dwtx.core.commands.operations.IOperationHistory; +import dwtx.core.commands.operations.IOperationHistoryListener; +import dwtx.core.commands.operations.IUndoContext; +import dwtx.core.commands.operations.IUndoableOperation; +import dwtx.core.commands.operations.ObjectUndoContext; +import dwtx.core.commands.operations.OperationHistoryEvent; +import dwtx.core.commands.operations.OperationHistoryFactory; +import dwtx.core.runtime.Assert; +import dwtx.core.runtime.IAdaptable; +import dwtx.core.runtime.IProgressMonitor; +import dwtx.core.runtime.IStatus; +import dwtx.core.runtime.ListenerList; +import dwtx.core.runtime.Status; +import dwtx.jface.text.BadLocationException; +import dwtx.jface.text.DocumentEvent; +import dwtx.jface.text.IDocument; +import dwtx.jface.text.IDocumentExtension4; +import dwtx.jface.text.IDocumentListener; +import dwtx.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 dwtx.jface.text.IDocument + * @since 3.2 + * @noextend This class is not intended to be subclassed by clients. + */ +public class DocumentUndoManager : 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 : 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; + } + + /* + * @see dwtx.core.commands.operations.IUndoableOperation#dispose() + */ + 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) { + } + } + + /* + * @see dwtx.core.commands.operations.IUndoableOperation#canUndo() + */ + public bool 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 + bool canUndo= docStamp is IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP + || docStamp is 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 is fDocumentUndoManager.fHistory + .getUndoOperation(fDocumentUndoManager.fUndoContext) + // this is the latest operation + && this !is 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 !is + // the invalid current operation has a document stamp + IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) { + canUndo= fDocumentUndoManager.fCurrent.fRedoModificationStamp is 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 is fDocumentUndoManager.fHistory + .getUndoOperation(fDocumentUndoManager.fUndoContext) + && // this is the latest operation + this instanceof UndoableCompoundTextChange + && this is fDocumentUndoManager.fCurrent + && // this is the current operation + this.fStart is -1 + && // the current operation text is not valid + fDocumentUndoManager.fCurrent.fRedoModificationStamp !is IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) { + // but it has a redo stamp + canUndo= fDocumentUndoManager.fCurrent.fRedoModificationStamp is docStamp; + } + return canUndo; + + } + // if there is no timestamp to check, simply return true per the + // 3.0.1 behavior + return true; + } + return false; + } + + /* + * @see dwtx.core.commands.operations.IUndoableOperation#canRedo() + */ + public bool canRedo() { + if (isValid()) { + if (fDocumentUndoManager.fDocument instanceof IDocumentExtension4) { + long docStamp= ((IDocumentExtension4) fDocumentUndoManager.fDocument) + .getModificationStamp(); + return docStamp is IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP + || docStamp is getUndoModificationStamp(); + } + // if there is no timestamp to check, simply return true per the + // 3.0.1 behavior + return true; + } + return false; + } + + /* + * @see dwtx.core.commands.operations.IUndoableOperation#canExecute() + */ + public bool canExecute() { + return fDocumentUndoManager.isConnected(); + } + + /* + * @see dwtx.core.commands.operations.IUndoableOperation.IUndoableOperation#execute(IProgressMonitor, IAdaptable) + */ + 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. + */ + 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 + */ + 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 bool 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 bool isValid() { + return fStart > -1 && fEnd > -1 && fText !is null; + } + + /* + * @see java.lang.Object#toString() + */ + 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 : 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); + } + + /* + * @see dwtx.text.undo.UndoableTextChange#undo(dwtx.core.runtime.IProgressMonitor, dwtx.core.runtime.IAdaptable) + */ + public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) { + + int size= fChanges.size(); + if (size > 0) { + UndoableTextChange c; + + c= (UndoableTextChange) 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= (UndoableTextChange) fChanges.get(i); + c.undoTextChange(); + } + fDocumentUndoManager.resetProcessChangeState(); + fDocumentUndoManager.fireDocumentUndo(c.fStart, c.fPreservedText, c.fText, uiInfo, + DocumentUndoEvent.UNDONE, true); + } + return Status.OK_STATUS; + } + + /* + * @see dwtx.text.undo.UndoableTextChange#redo(dwtx.core.runtime.IProgressMonitor, dwtx.core.runtime.IAdaptable) + */ + public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) { + + int size= fChanges.size(); + if (size > 0) { + + UndoableTextChange c; + c= (UndoableTextChange) 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= (UndoableTextChange) fChanges.get(i); + c.redoTextChange(); + } + fDocumentUndoManager.resetProcessChangeState(); + fDocumentUndoManager.fireDocumentUndo(c.fStart, c.fText, c.fPreservedText, uiInfo, DocumentUndoEvent.REDONE, true); + } + + return Status.OK_STATUS; + } + + /* + * @see dwtx.text.undo.UndoableTextChange#updateTextChange() + */ + 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(); + } + + /* + * @see dwtx.text.undo.UndoableTextChange#createCurrent() + */ + protected UndoableTextChange createCurrent() { + + if (!fDocumentUndoManager.fFoldingIntoCompoundChange) + return new UndoableTextChange(fDocumentUndoManager); + + reinitialize(); + return this; + } + + /* + * @see dwtx.text.undo.UndoableTextChange#commit() + */ + protected void commit() { + // if there is pending data, update the text change + if (fStart > -1) + updateTextChange(); + fDocumentUndoManager.fCurrent= createCurrent(); + fDocumentUndoManager.resetProcessChangeState(); + } + + /* + * @see dwtx.text.undo.UndoableTextChange#isValid() + */ + protected bool isValid() { + return fStart > -1 || fChanges.size() > 0; + } + + /* + * @see dwtx.text.undo.UndoableTextChange#getUndoModificationStamp() + */ + protected long getUndoModificationStamp() { + if (fStart > -1) + return super.getUndoModificationStamp(); + else if (fChanges.size() > 0) + return ((UndoableTextChange) fChanges.get(0)) + .getUndoModificationStamp(); + + return fUndoModificationStamp; + } + + /* + * @see dwtx.text.undo.UndoableTextChange#getRedoModificationStamp() + */ + protected long getRedoModificationStamp() { + if (fStart > -1) + return super.getRedoModificationStamp(); + else if (fChanges.size() > 0) + return ((UndoableTextChange) fChanges.get(fChanges.size() - 1)) + .getRedoModificationStamp(); + + return fRedoModificationStamp; + } + } + + + /** + * Internal listener to document changes. + */ + private class DocumentListener : IDocumentListener { + + private String fReplacedText; + + /* + * @see dwtx.jface.text.IDocumentListener#documentAboutToBeChanged(dwtx.jface.text.DocumentEvent) + */ + public void documentAboutToBeChanged(DocumentEvent event) { + try { + fReplacedText= event.getDocument().get(event.getOffset(), + event.getLength()); + fPreservedUndoModificationStamp= event.getModificationStamp(); + } catch (BadLocationException x) { + fReplacedText= null; + } + } + + /* + * @see dwtx.jface.text.IDocumentListener#documentChanged(dwtx.jface.text.DocumentEvent) + */ + 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); + bool wasValid= false; + if (op !is 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 is 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 !is 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 !is fLastAddedTextEdit && fCurrent.isValid()) { + addToOperationHistory(fCurrent); + } + } + } + } + + /* + * @see IOperationHistoryListener + */ + private class HistoryListener : IOperationHistoryListener { + + private IUndoableOperation fOperation; + + 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 is 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() is 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 bool 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 bool fInserting= false; + + /** Indicates overwriting state. */ + private bool fOverwriting= false; + + /** The registered document listeners. */ + private ListenerList fDocumentUndoListeners; + + /** The list of clients connected. */ + private List 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); + } + + /* + * @see dwtx.jface.text.IDocumentUndoManager#addDocumentUndoListener(dwtx.jface.text.IDocumentUndoListener) + */ + public void addDocumentUndoListener(IDocumentUndoListener listener) { + fDocumentUndoListeners.add(listener); + } + + /* + * @see dwtx.jface.text.IDocumentUndoManager#removeDocumentUndoListener(dwtx.jface.text.IDocumentUndoListener) + */ + public void removeDocumentUndoListener(IDocumentUndoListener listener) { + fDocumentUndoListeners.remove(listener); + } + + /* + * @see dwtx.jface.text.IDocumentUndoManager#getUndoContext() + */ + public IUndoContext getUndoContext() { + return fUndoContext; + } + + /* + * @see dwtx.jface.text.IDocumentUndoManager#commit() + */ + 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 !is fCurrent) { + fCurrent.pretendCommit(); + if (fCurrent.isValid()) + addToOperationHistory(fCurrent); + } + fCurrent.commit(); + } + + /* + * @see dwtx.text.undo.IDocumentUndoManager#reset() + */ + public void reset() { + if (isConnected()) { + shutdown(); + initialize(); + } + } + + /* + * @see dwtx.text.undo.IDocumentUndoManager#redoable() + */ + public bool redoable() { + return OperationHistoryFactory.getOperationHistory().canRedo(fUndoContext); + } + + /* + * @see dwtx.text.undo.IDocumentUndoManager#undoable() + */ + public bool undoable() { + return OperationHistoryFactory.getOperationHistory().canUndo(fUndoContext); + } + + /* + * @see dwtx.text.undo.IDocumentUndoManager#undo() + */ + public void redo() throws ExecutionException { + if (isConnected() && redoable()) + OperationHistoryFactory.getOperationHistory().redo(getUndoContext(), null, null); + } + + /* + * @see dwtx.text.undo.IDocumentUndoManager#undo() + */ + public void undo() throws ExecutionException { + if (undoable()) + OperationHistoryFactory.getOperationHistory().undo(fUndoContext, null, null); + } + + /* + * @see dwtx.jface.text.IDocumentUndoManager#connect(java.lang.Object) + */ + public void connect(Object client) { + if (!isConnected()) { + initialize(); + } + if (!fConnected.contains(client)) + fConnected.add(client); + } + + /* + * @see dwtx.jface.text.IDocumentUndoManager#disconnect(java.lang.Object) + */ + public void disconnect(Object client) { + fConnected.remove(client); + if (!isConnected()) { + shutdown(); + } + } + + /* + * @see dwtx.jface.text.IDocumentUndoManager#beginCompoundChange() + */ + public void beginCompoundChange() { + if (isConnected()) { + fFoldingIntoCompoundChange= true; + commit(); + } + } + + /* + * @see dwtx.jface.text.IDocumentUndoManager#endCompoundChange() + */ + public void endCompoundChange() { + if (isConnected()) { + fFoldingIntoCompoundChange= false; + commit(); + } + } + + /* + * @see dwtx.jface.text.IDocumentUndoManager#setUndoLimit(int) + */ + 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, bool 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 !is null && fUndoContext !is 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 bool isWhitespaceText(String text) { + + if (text is null || text.length() is 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 !is ' ' && c !is '\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(bool listen) { + if (listen) { + if (fDocumentListener is null && fDocument !is null) { + fDocumentListener= new DocumentListener(); + fDocument.addDocumentListener(fDocumentListener); + } + } else if (!listen) { + if (fDocumentListener !is null && fDocument !is null) { + fDocument.removeDocumentListener(fDocumentListener); + fDocumentListener= null; + } + } + } + + private void processChange(int modelStart, int modelEnd, + String insertedText, String replacedText, + long beforeChangeModificationStamp, + long afterChangeModificationStamp) { + + if (insertedText is null) + insertedText= ""; //$NON-NLS-1$ + + if (replacedText is null) + replacedText= ""; //$NON-NLS-1$ + + int length= insertedText.length(); + int diff= modelEnd - modelStart; + + if (fCurrent.fUndoModificationStamp is IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) + fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; + + // normalize + if (diff < 0) { + int tmp= modelEnd; + modelEnd= modelStart; + modelStart= tmp; + } + + if (modelStart is modelEnd) { + // text will be inserted + if ((length is 1) || isWhitespaceText(insertedText)) { + // by typing or whitespace + if (!fInserting + || (modelStart !is 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 is 0) { + // text will be deleted by backspace or DEL key or empty + // clipboard + length= replacedText.length(); + String[] delimiters= fDocument.getLegalLineDelimiters(); + + if ((length is 1) + || TextUtilities.equals(delimiters, replacedText) > -1) { + + // whereby selection is empty + + if (fPreviousDelete.fStart is modelStart + && fPreviousDelete.fEnd is modelEnd) { + // repeated DEL + + // correct wrong settings of fCurrent + if (fCurrent.fStart is modelEnd + && fCurrent.fEnd is modelStart) { + fCurrent.fStart= modelStart; + fCurrent.fEnd= modelEnd; + } + // append to buffer && extend edit range + fPreservedTextBuffer.append(replacedText); + ++fCurrent.fEnd; + + } else if (fPreviousDelete.fStart is 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 is 1) { + length= replacedText.length(); + String[] delimiters= fDocument.getLegalLineDelimiters(); + + if ((length is 1) + || TextUtilities.equals(delimiters, replacedText) > -1) { + // because of overwrite mode or model manipulation + if (!fOverwriting + || (modelStart !is 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 + */ + bool isConnected() { + if (fConnected is null) + return false; + return !fConnected.isEmpty(); + } + + /* + * @see dwtx.jface.text.IDocumentUndoManager#transferUndoHistory(IDocumentUndoManager) + */ + 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 !is 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 !is null) + cmd.fUndoModificationStamp= ((UndoableTextChange)op).fRedoModificationStamp; + } + addToOperationHistory(cmd); + } + + +}