| /******************************************************************************* |
| * Copyright (c) 2001, 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 |
| * Jens Lukowski/Innoopract - initial renaming/restructuring |
| * Jesper Steen Møller - initial IDocumentExtension4 support - #102822 |
| * |
| *******************************************************************************/ |
| package org.eclipse.wst.sse.core.internal.undo; |
| |
| import java.util.EventObject; |
| |
| import org.eclipse.emf.common.command.BasicCommandStack; |
| import org.eclipse.emf.common.command.Command; |
| import org.eclipse.emf.common.command.CommandStack; |
| import org.eclipse.emf.common.command.CommandStackListener; |
| import org.eclipse.emf.common.command.CompoundCommand; |
| import org.eclipse.jface.text.DocumentRewriteSession; |
| import org.eclipse.jface.text.DocumentRewriteSessionType; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IDocumentExtension4; |
| import org.eclipse.wst.sse.core.StructuredModelManager; |
| import org.eclipse.wst.sse.core.internal.SSECoreMessages; |
| import org.eclipse.wst.sse.core.internal.provisional.IModelManager; |
| import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; |
| import org.eclipse.wst.sse.core.internal.provisional.events.IStructuredDocumentListener; |
| import org.eclipse.wst.sse.core.internal.provisional.events.NewDocumentEvent; |
| import org.eclipse.wst.sse.core.internal.provisional.events.NoChangeEvent; |
| import org.eclipse.wst.sse.core.internal.provisional.events.RegionChangedEvent; |
| import org.eclipse.wst.sse.core.internal.provisional.events.RegionsReplacedEvent; |
| import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentEvent; |
| import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentRegionsReplacedEvent; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; |
| import org.eclipse.wst.sse.core.internal.util.Assert; |
| import org.eclipse.wst.sse.core.internal.util.Utilities; |
| |
| public class StructuredTextUndoManager implements IStructuredTextUndoManager { |
| |
| class InternalCommandStackListener implements CommandStackListener { |
| public void commandStackChanged(EventObject event) { |
| resetInternalCommands(); |
| } |
| } |
| |
| class InternalStructuredDocumentListener implements IStructuredDocumentListener { |
| |
| public void newModel(NewDocumentEvent structuredDocumentEvent) { |
| // Do nothing. Do not push the new model's structuredDocument |
| // changes |
| // onto the undo command stack, or else the user may be able to |
| // undo |
| // an existing file to an empty file. |
| } |
| |
| public void noChange(NoChangeEvent structuredDocumentEvent) { |
| // Since "no change", do nothing. |
| } |
| |
| public void nodesReplaced(StructuredDocumentRegionsReplacedEvent structuredDocumentEvent) { |
| processStructuredDocumentEvent(structuredDocumentEvent); |
| } |
| |
| private void processStructuredDocumentEvent(String textDeleted, String textInserted, int textStart, int textEnd) { |
| if (fTextCommand != null && textStart == fTextCommand.getTextEnd()) { |
| // append to the text command |
| fTextCommand.setTextDeleted(fTextCommand.getTextDeleted().concat(textDeleted)); |
| fTextCommand.setTextInserted(fTextCommand.getTextInserted().concat(textInserted)); |
| fTextCommand.setTextEnd(textEnd); |
| } |
| else if (fTextCommand != null && textStart == fTextCommand.getTextStart() - 1 && textEnd <= fTextCommand.getTextEnd() - 1 && textDeleted.length() == 1 && textInserted.length() == 0 && fTextCommand.getTextDeleted().length() > 0) { |
| // backspace pressed |
| // erase a character in the file |
| fTextCommand.setTextDeleted(textDeleted.concat(fTextCommand.getTextDeleted())); |
| fTextCommand.setTextStart(textStart); |
| } |
| else { |
| createNewTextCommand(textDeleted, textInserted, textStart, textEnd); |
| } |
| |
| // save cursor position |
| fCursorPosition = textEnd; |
| } |
| |
| private void processStructuredDocumentEvent(StructuredDocumentEvent structuredDocumentEvent) { |
| // Note: fListening tells us if we should listen to the |
| // StructuredDocumentEvent. |
| // fListening is set to false right before the undo/redo process |
| // and |
| // then set to true again |
| // right after the undo/redo process to block out and ignore all |
| // StructuredDocumentEvents generated |
| // by the undo/redo process. |
| |
| // Process StructuredDocumentEvent if fListening is true. |
| // |
| // We are executing a command from the command stack if the |
| // requester |
| // is a command (for example, undo/redo). |
| // We should not process the flat model event when we are |
| // executing a |
| // command from the command stack. |
| if (fUndoManagementEnabled && !(structuredDocumentEvent.getOriginalRequester() instanceof Command)) { |
| // check requester if not recording |
| if (!fRecording) |
| checkRequester(structuredDocumentEvent.getOriginalRequester()); |
| |
| // process the structuredDocumentEvent |
| String textDeleted = structuredDocumentEvent.getDeletedText(); |
| String textInserted = structuredDocumentEvent.getText(); |
| int textStart = structuredDocumentEvent.getOffset(); |
| int textEnd = textStart + textInserted.length(); |
| processStructuredDocumentEvent(textDeleted, textInserted, textStart, textEnd); |
| } |
| } |
| |
| public void regionChanged(RegionChangedEvent structuredDocumentEvent) { |
| processStructuredDocumentEvent(structuredDocumentEvent); |
| } |
| |
| public void regionsReplaced(RegionsReplacedEvent structuredDocumentEvent) { |
| processStructuredDocumentEvent(structuredDocumentEvent); |
| } |
| |
| } |
| |
| private static final String TEXT_CHANGE_TEXT = SSECoreMessages.Text_Change_UI_; //$NON-NLS-1$ |
| private CommandStack fCommandStack = null; |
| private StructuredTextCompoundCommandImpl fCompoundCommand = null; |
| private String fCompoundCommandDescription = null; |
| private String fCompoundCommandLabel = null; |
| int fCursorPosition = 0; |
| // private IStructuredModel fStructuredModel = null; |
| private IDocument fDocument; |
| private InternalCommandStackListener fInternalCommandStackListener; |
| // private Map fTextViewerToListenerMap = new HashMap(); |
| private IStructuredDocumentListener fInternalStructuredDocumentListener; |
| private IDocumentSelectionMediator[] fMediators = null; |
| private boolean fRecording = false; |
| private int fRecordingCount = 0; |
| private Object fRequester; |
| StructuredTextCommandImpl fTextCommand = null; |
| private int fUndoCursorPosition = -1; |
| boolean fUndoManagementEnabled = true; |
| private int fUndoSelectionLength = 0; |
| |
| public StructuredTextUndoManager() { |
| this(new BasicCommandStack()); |
| } |
| |
| public StructuredTextUndoManager(CommandStack commandStack) { |
| setCommandStack(commandStack); |
| } |
| |
| private void addDocumentSelectionMediator(IDocumentSelectionMediator mediator) { |
| if (!Utilities.contains(fMediators, mediator)) { |
| int oldSize = 0; |
| |
| if (fMediators != null) { |
| // normally won't be null, but we need to be sure, for first |
| // time through |
| oldSize = fMediators.length; |
| } |
| |
| int newSize = oldSize + 1; |
| IDocumentSelectionMediator[] newMediators = new IDocumentSelectionMediator[newSize]; |
| if (fMediators != null) { |
| System.arraycopy(fMediators, 0, newMediators, 0, oldSize); |
| } |
| |
| // add the new undo mediator to last position |
| newMediators[newSize - 1] = mediator; |
| |
| // now switch new for old |
| fMediators = newMediators; |
| } |
| else { |
| removeDocumentSelectionMediator(mediator); |
| addDocumentSelectionMediator(mediator); |
| } |
| } |
| |
| public void beginRecording(Object requester) { |
| beginRecording(requester, null, null); |
| } |
| |
| public void beginRecording(Object requester, int cursorPosition, int selectionLength) { |
| beginRecording(requester, null, null); |
| |
| fUndoCursorPosition = cursorPosition; |
| fUndoSelectionLength = selectionLength; |
| } |
| |
| public void beginRecording(Object requester, String label) { |
| beginRecording(requester, label, null); |
| } |
| |
| public void beginRecording(Object requester, String label, int cursorPosition, int selectionLength) { |
| beginRecording(requester, label, null); |
| |
| fUndoCursorPosition = cursorPosition; |
| fUndoSelectionLength = selectionLength; |
| } |
| |
| public void beginRecording(Object requester, String label, String description) { |
| // save the requester |
| fRequester = requester; |
| |
| // update label and desc only on the first level when recording is |
| // nested |
| if (fRecordingCount == 0) { |
| fCompoundCommandLabel = label; |
| if (fCompoundCommandLabel == null) |
| fCompoundCommandLabel = TEXT_CHANGE_TEXT; |
| |
| fCompoundCommandDescription = description; |
| if (fCompoundCommandDescription == null) |
| fCompoundCommandDescription = TEXT_CHANGE_TEXT; |
| |
| // clear commands |
| fTextCommand = null; |
| fCompoundCommand = null; |
| } |
| |
| // update counter and flag |
| fRecordingCount++; |
| fRecording = true; |
| |
| // no undo cursor position and undo selection length specified |
| // reset undo cursor position and undo selection length |
| fUndoCursorPosition = -1; |
| fUndoSelectionLength = 0; |
| } |
| |
| public void beginRecording(Object requester, String label, String description, int cursorPosition, int selectionLength) { |
| beginRecording(requester, label, description); |
| |
| fUndoCursorPosition = cursorPosition; |
| fUndoSelectionLength = selectionLength; |
| } |
| |
| void checkRequester(Object requester) { |
| if (fRequester != null && !fRequester.equals(requester)) { |
| // Force restart of recording so the last compound command is |
| // closed. |
| // |
| // However, we should not force restart of recording when the |
| // request came from StructuredDocumentToTextAdapter or |
| // XMLModelImpl |
| // because cut/paste requests and character inserts to the |
| // textViewer are from StructuredDocumentToTextAdapter, |
| // and requests to delete a node in the XMLTableTreeViewer are |
| // from XMLModelImpl (which implements IStructuredModel). |
| |
| if (!(requester instanceof IStructuredModel || requester instanceof IStructuredDocument)) { |
| resetInternalCommands(); |
| } |
| } |
| } |
| |
| |
| |
| public void connect(IDocumentSelectionMediator mediator) { |
| Assert.isNotNull(mediator); |
| if (fDocument == null) { |
| // add this undo manager as structured document listener |
| fDocument = mediator.getDocument(); |
| // future_TODO: eventually we want to refactor or allow either |
| // type of document, but for now, we'll do instanceof check, and |
| // fail |
| // if not right type |
| if (fDocument instanceof IStructuredDocument) { |
| ((IStructuredDocument) fDocument).addDocumentChangedListener(getInternalStructuredDocumentListener()); |
| } |
| else { |
| throw new IllegalArgumentException("only meditator with structured documents currently handled"); //$NON-NLS-1$ |
| } |
| } |
| else { |
| // if we've already had our document set, we'll just do this fail |
| // fast integrity check |
| if (!fDocument.equals(mediator.getDocument())) |
| throw new IllegalStateException("Connection to undo manager failed. Document for document selection mediator inconistent with undo manager."); //$NON-NLS-1$ |
| } |
| |
| addDocumentSelectionMediator(mediator); |
| } |
| |
| void createNewTextCommand(String textDeleted, String textInserted, int textStart, int textEnd) { |
| StructuredTextCommandImpl textCommand = new StructuredTextCommandImpl(fDocument); |
| textCommand.setLabel(TEXT_CHANGE_TEXT); |
| textCommand.setDescription(TEXT_CHANGE_TEXT); |
| textCommand.setTextStart(textStart); |
| textCommand.setTextEnd(textEnd); |
| textCommand.setTextDeleted(textDeleted); |
| textCommand.setTextInserted(textInserted); |
| |
| if (fRecording) { |
| if (fCompoundCommand == null) { |
| StructuredTextCompoundCommandImpl compoundCommand = new StructuredTextCompoundCommandImpl(); |
| compoundCommand.setUndoCursorPosition(fUndoCursorPosition); |
| compoundCommand.setUndoSelectionLength(fUndoSelectionLength); |
| |
| compoundCommand.setLabel(fCompoundCommandLabel); |
| compoundCommand.setDescription(fCompoundCommandDescription); |
| compoundCommand.append(textCommand); |
| |
| fCompoundCommand = compoundCommand; |
| } |
| else { |
| fCompoundCommand.append(textCommand); |
| } |
| } |
| else { |
| fCommandStack.execute(textCommand); |
| } |
| |
| fTextCommand = textCommand; |
| } |
| |
| /** |
| * Disable undo management. |
| */ |
| public void disableUndoManagement() { |
| fUndoManagementEnabled = false; |
| } |
| |
| public void disconnect(IDocumentSelectionMediator mediator) { |
| removeDocumentSelectionMediator(mediator); |
| |
| if (fMediators != null && fMediators.length == 0 && fDocument != null) { |
| // remove this undo manager as structured document listener |
| // future_TODO: eventually we want to refactor or allow either |
| // type of document, but for now, we'll do instanceof check, and |
| // fail |
| // if not right type |
| if (fDocument instanceof IStructuredDocument) { |
| ((IStructuredDocument) fDocument).removeDocumentChangedListener(getInternalStructuredDocumentListener()); |
| } |
| else { |
| throw new IllegalArgumentException("only meditator with structured documents currently handled"); //$NON-NLS-1$ |
| } |
| // if no longer listening to document, then dont even track it |
| // anymore |
| // (this allows connect to reconnect to document again) |
| fDocument = null; |
| } |
| } |
| |
| public void enableUndoManagement() { |
| fUndoManagementEnabled = true; |
| } |
| |
| public void endRecording(Object requester) { |
| int cursorPosition = (fTextCommand != null) ? fTextCommand.getTextEnd() : -1; |
| int selectionLength = 0; |
| |
| endRecording(requester, cursorPosition, selectionLength); |
| } |
| |
| public void endRecording(Object requester, int cursorPosition, int selectionLength) { |
| // Recording could be stopped by forceEndOfPendingCommand(). Make sure |
| // we are still recording before proceeding, or else fRecordingCount |
| // may not be balanced. |
| if (fRecording) { |
| if (fCompoundCommand != null) { |
| fCompoundCommand.setRedoCursorPosition(cursorPosition); |
| fCompoundCommand.setRedoSelectionLength(selectionLength); |
| } |
| |
| // end recording is a logical stopping point for text command, |
| // even when fRecordingCount > 0 (in nested beginRecording) |
| fTextCommand = null; |
| |
| // update counter and flag |
| if (fRecordingCount > 0) |
| fRecordingCount--; |
| if (fRecordingCount == 0) { |
| |
| // Finally execute the commands accumulated in the compound command. |
| |
| if (fCompoundCommand != null) { |
| fCommandStack.execute(fCompoundCommand); |
| } |
| |
| fRecording = false; |
| |
| // reset compound command only when fRecordingCount == |
| // 0 |
| fCompoundCommand = null; |
| fCompoundCommandLabel = null; |
| fCompoundCommandDescription = null; |
| |
| // Also reset fRequester |
| fRequester = null; |
| } |
| } |
| } |
| |
| /** |
| * Utility method to find model given document |
| */ |
| private IStructuredModel findStructuredModel(IDocument document) { |
| IModelManager modelManager = StructuredModelManager.getModelManager(); |
| IStructuredModel structuredModel = modelManager.getExistingModelForRead(document); |
| return structuredModel; |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.eclipse.wst.sse.core.undo.IStructuredTextUndoManager#forceEndOfPendingCommand(java.lang.Object, |
| * int, int) |
| */ |
| public void forceEndOfPendingCommand(Object requester, int currentPosition, int length) { |
| if (fRecording) |
| endRecording(requester, currentPosition, length); |
| else |
| resetInternalCommands(); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.eclipse.wst.sse.core.undo.IStructuredTextUndoManager#getCommandStack() |
| */ |
| public CommandStack getCommandStack() { |
| return fCommandStack; |
| } |
| |
| /** |
| * @return |
| */ |
| private CommandStackListener getInternalCommandStackListener() { |
| if (fInternalCommandStackListener == null) { |
| fInternalCommandStackListener = new InternalCommandStackListener(); |
| } |
| return fInternalCommandStackListener; |
| } |
| |
| /** |
| * @return |
| */ |
| private IStructuredDocumentListener getInternalStructuredDocumentListener() { |
| if (fInternalStructuredDocumentListener == null) { |
| fInternalStructuredDocumentListener = new InternalStructuredDocumentListener(); |
| } |
| return fInternalStructuredDocumentListener; |
| } |
| |
| public Command getRedoCommand() { |
| return fCommandStack.getRedoCommand(); |
| } |
| |
| public Command getUndoCommand() { |
| return fCommandStack.getUndoCommand(); |
| } |
| |
| public void redo() { |
| redo(null); |
| } |
| |
| public void redo(IDocumentSelectionMediator requester) { |
| IStructuredModel model = findStructuredModel(fDocument); |
| |
| if (redoable()) { |
| IDocumentExtension4 docExt4 = null; |
| DocumentRewriteSession rewriteSession = null; |
| try { |
| if (model != null) |
| model.aboutToChangeModel(); |
| |
| Command redoCommand = getRedoCommand(); |
| if (redoCommand instanceof CompoundCommand && |
| model.getStructuredDocument() instanceof IDocumentExtension4) { |
| docExt4 = (IDocumentExtension4)model.getStructuredDocument(); |
| } |
| rewriteSession = (docExt4 == null) ? null : |
| docExt4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED); |
| |
| // make sure to redo before setting document selection |
| fCommandStack.redo(); |
| |
| // set document selection |
| setRedoDocumentSelection(requester, redoCommand); |
| } |
| finally { |
| if (docExt4 != null && rewriteSession != null) |
| docExt4.stopRewriteSession(rewriteSession); |
| if (model != null) { |
| model.changedModel(); |
| model.releaseFromRead(); |
| } |
| } |
| } |
| } |
| |
| public boolean redoable() { |
| return fCommandStack.canRedo(); |
| } |
| |
| private void removeDocumentSelectionMediator(IDocumentSelectionMediator mediator) { |
| if (fMediators != null && mediator != null) { |
| // if its not in the array, we'll ignore the request |
| if (Utilities.contains(fMediators, mediator)) { |
| int oldSize = fMediators.length; |
| int newSize = oldSize - 1; |
| IDocumentSelectionMediator[] newMediators = new IDocumentSelectionMediator[newSize]; |
| int index = 0; |
| for (int i = 0; i < oldSize; i++) { |
| if (fMediators[i] == mediator) { // ignore |
| } |
| else { |
| // copy old to new if its not the one we are removing |
| newMediators[index++] = fMediators[i]; |
| } |
| } |
| // now that we have a new array, let's switch it for the old |
| // one |
| fMediators = newMediators; |
| } |
| } |
| } |
| |
| void resetInternalCommands() { |
| // Either the requester of the structured document change event is |
| // changed, or the command stack is changed. Need to reset internal |
| // commands so we won't continue to append changes. |
| fCompoundCommand = null; |
| fTextCommand = null; |
| |
| // Also reset fRequester |
| fRequester = null; |
| } |
| |
| public void setCommandStack(CommandStack commandStack) { |
| if (fCommandStack != null) |
| fCommandStack.removeCommandStackListener(getInternalCommandStackListener()); |
| |
| fCommandStack = commandStack; |
| |
| if (fCommandStack != null) |
| fCommandStack.addCommandStackListener(getInternalCommandStackListener()); |
| } |
| |
| private void setRedoDocumentSelection(IDocumentSelectionMediator requester, Command command) { |
| int cursorPosition = -1; |
| int selectionLength = 0; |
| |
| if (command instanceof CommandCursorPosition) { |
| CommandCursorPosition commandCursorPosition = (CommandCursorPosition) command; |
| cursorPosition = commandCursorPosition.getRedoCursorPosition(); |
| selectionLength = commandCursorPosition.getRedoSelectionLength(); |
| } |
| else if (command instanceof StructuredTextCommand) { |
| StructuredTextCommand structuredTextCommand = (StructuredTextCommand) command; |
| cursorPosition = structuredTextCommand.getTextStart(); |
| selectionLength = structuredTextCommand.getTextInserted().length(); |
| } |
| |
| if (cursorPosition > -1 && fMediators != null && fMediators.length > 0) { |
| for (int i = 0; i < fMediators.length; i++) { |
| IDocument document = fMediators[i].getDocument(); |
| fMediators[i].undoOperationSelectionChanged(new UndoDocumentEvent(requester, document, cursorPosition, selectionLength)); |
| } |
| } |
| } |
| |
| private void setUndoDocumentSelection(IDocumentSelectionMediator requester, Command command) { |
| int cursorPosition = -1; |
| int selectionLength = 0; |
| |
| if (command instanceof CommandCursorPosition) { |
| CommandCursorPosition commandCursorPosition = (CommandCursorPosition) command; |
| cursorPosition = commandCursorPosition.getUndoCursorPosition(); |
| selectionLength = commandCursorPosition.getUndoSelectionLength(); |
| } |
| else if (command instanceof StructuredTextCommand) { |
| StructuredTextCommand structuredTextCommand = (StructuredTextCommand) command; |
| cursorPosition = structuredTextCommand.getTextStart(); |
| selectionLength = structuredTextCommand.getTextDeleted().length(); |
| } |
| |
| if (cursorPosition > -1 && fMediators != null && fMediators.length > 0) { |
| for (int i = 0; i < fMediators.length; i++) { |
| IDocument document = fMediators[i].getDocument(); |
| fMediators[i].undoOperationSelectionChanged(new UndoDocumentEvent(requester, document, cursorPosition, selectionLength)); |
| } |
| } |
| } |
| |
| public void undo() { |
| undo(null); |
| } |
| |
| public void undo(IDocumentSelectionMediator requester) { |
| // Force an endRecording before undo. |
| // |
| // For example, recording was turned on on the Design Page of |
| // PageDesigner. |
| // Then undo is invoked on the Source Page. Recording should be |
| // stopped before we undo. |
| // Note that redo should not be available when we switch to the Source |
| // Page. |
| // Therefore, this force ending of recording is not needed in redo. |
| if (fRecording) |
| endRecording(this); |
| |
| if (undoable()) { |
| IStructuredModel model = findStructuredModel(fDocument); |
| IDocumentExtension4 docExt4 = null; |
| DocumentRewriteSession rewriteSession = null; |
| |
| try { |
| if (model != null) |
| model.aboutToChangeModel(); |
| |
| Command undoCommand = getUndoCommand(); |
| if (undoCommand instanceof CompoundCommand && |
| model.getStructuredDocument() instanceof IDocumentExtension4) { |
| docExt4 = (IDocumentExtension4)model.getStructuredDocument(); |
| } |
| rewriteSession = (docExt4 == null) ? null : |
| docExt4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED); |
| |
| // make sure to undo before setting document selection |
| fCommandStack.undo(); |
| |
| // set document selection |
| setUndoDocumentSelection(requester, undoCommand); |
| } |
| finally { |
| if (docExt4 != null && rewriteSession != null) |
| docExt4.stopRewriteSession(rewriteSession); |
| if (model != null) { |
| model.changedModel(); |
| model.releaseFromRead(); |
| } |
| } |
| } |
| } |
| |
| public boolean undoable() { |
| return fCommandStack.canUndo(); |
| } |
| } |