Package org.eclipse.text.undo

Source Code of org.eclipse.text.undo.DocumentUndoManager$HistoryListener

/*******************************************************************************
* Copyright (c) 2006, 2009 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;
    }

    /*
     * @see org.eclipse.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 org.eclipse.core.commands.operations.IUndoableOperation#canUndo()
     */
    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;
    }

    /*
     * @see org.eclipse.core.commands.operations.IUndoableOperation#canRedo()
     */
    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;
    }

    /*
     * @see org.eclipse.core.commands.operations.IUndoableOperation#canExecute()
     */
    public boolean canExecute() {
      return fDocumentUndoManager.isConnected();
    }

    /*
     * @see org.eclipse.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 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;
    }

    /*
     * @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 extends UndoableTextChange {

    /** The list of individual changes */
    private List fChanges= new ArrayList();

    /**
     * Creates a new compound text change.
     *
     * @param manager the undo manager for this change
     */
    UndoableCompoundTextChange(DocumentUndoManager manager) {
      super(manager);
    }

    /**
     * Adds a new individual change to this compound change.
     *
     * @param change the change to be added
     */
    protected void add(UndoableTextChange change) {
      fChanges.add(change);
    }

    /*
     * @see org.eclipse.text.undo.UndoableTextChange#undo(org.eclipse.core.runtime.IProgressMonitor, org.eclipse.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 org.eclipse.text.undo.UndoableTextChange#redo(org.eclipse.core.runtime.IProgressMonitor, org.eclipse.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 org.eclipse.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 org.eclipse.text.undo.UndoableTextChange#createCurrent()
     */
    protected UndoableTextChange createCurrent() {

      if (!fDocumentUndoManager.fFoldingIntoCompoundChange)
        return new UndoableTextChange(fDocumentUndoManager);

      reinitialize();
      return this;
    }

    /*
     * @see org.eclipse.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 org.eclipse.text.undo.UndoableTextChange#isValid()
     */
    protected boolean isValid() {
      return fStart > -1 || fChanges.size() > 0;
    }

    /*
     * @see org.eclipse.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 org.eclipse.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 implements IDocumentListener {

    private String fReplacedText;

    /*
     * @see org.eclipse.jface.text.IDocumentListener#documentAboutToBeChanged(org.eclipse.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 org.eclipse.jface.text.IDocumentListener#documentChanged(org.eclipse.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);
      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;

    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 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 org.eclipse.jface.text.IDocumentUndoManager#addDocumentUndoListener(org.eclipse.jface.text.IDocumentUndoListener)
   */
  public void addDocumentUndoListener(IDocumentUndoListener listener) {
    fDocumentUndoListeners.add(listener);
  }

  /*
   * @see org.eclipse.jface.text.IDocumentUndoManager#removeDocumentUndoListener(org.eclipse.jface.text.IDocumentUndoListener)
   */
  public void removeDocumentUndoListener(IDocumentUndoListener listener) {
    fDocumentUndoListeners.remove(listener);
  }

  /*
   * @see org.eclipse.jface.text.IDocumentUndoManager#getUndoContext()
   */
  public IUndoContext getUndoContext() {
    return fUndoContext;
  }

  /*
   * @see org.eclipse.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 != fCurrent) {
      fCurrent.pretendCommit();
      if (fCurrent.isValid())
        addToOperationHistory(fCurrent);
    }
    fCurrent.commit();
  }

  /*
   * @see org.eclipse.text.undo.IDocumentUndoManager#reset()
   */
  public void reset() {
    if (isConnected()) {
      shutdown();
      initialize();
    }
  }

  /*
   * @see org.eclipse.text.undo.IDocumentUndoManager#redoable()
   */
  public boolean redoable() {
    return OperationHistoryFactory.getOperationHistory().canRedo(fUndoContext);
  }

  /*
   * @see org.eclipse.text.undo.IDocumentUndoManager#undoable()
   */
  public boolean undoable() {
    return OperationHistoryFactory.getOperationHistory().canUndo(fUndoContext);
  }

  /*
   * @see org.eclipse.text.undo.IDocumentUndoManager#undo()
   */
  public void redo() throws ExecutionException {
    if (isConnected() && redoable())
      OperationHistoryFactory.getOperationHistory().redo(getUndoContext(), null, null);
  }

  /*
   * @see org.eclipse.text.undo.IDocumentUndoManager#undo()
   */
  public void undo() throws ExecutionException {
    if (undoable())
      OperationHistoryFactory.getOperationHistory().undo(fUndoContext, null, null);
  }

  /*
   * @see org.eclipse.jface.text.IDocumentUndoManager#connect(java.lang.Object)
   */
  public void connect(Object client) {
    if (!isConnected()) {
      initialize();
    }
    if (!fConnected.contains(client))
      fConnected.add(client);
  }

  /*
   * @see org.eclipse.jface.text.IDocumentUndoManager#disconnect(java.lang.Object)
   */
  public void disconnect(Object client) {
    fConnected.remove(client);
    if (!isConnected()) {
      shutdown();
    }
  }

  /*
   * @see org.eclipse.jface.text.IDocumentUndoManager#beginCompoundChange()
   */
  public void beginCompoundChange() {
    if (isConnected()) {
      fFoldingIntoCompoundChange= true;
      commit();
    }
  }

  /*
   * @see org.eclipse.jface.text.IDocumentUndoManager#endCompoundChange()
   */
  public void endCompoundChange() {
    if (isConnected()) {
      fFoldingIntoCompoundChange= false;
      commit();
    }
  }

  /*
   * @see org.eclipse.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, 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();
  }

  /*
   * @see org.eclipse.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 != 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);
  }


}
TOP

Related Classes of org.eclipse.text.undo.DocumentUndoManager$HistoryListener

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.